commit
94b719e150
239 changed files with 10393 additions and 2204 deletions
13
.coveragerc
13
.coveragerc
|
@ -40,9 +40,6 @@ omit =
|
||||||
homeassistant/components/isy994.py
|
homeassistant/components/isy994.py
|
||||||
homeassistant/components/*/isy994.py
|
homeassistant/components/*/isy994.py
|
||||||
|
|
||||||
homeassistant/components/litejet.py
|
|
||||||
homeassistant/components/*/litejet.py
|
|
||||||
|
|
||||||
homeassistant/components/modbus.py
|
homeassistant/components/modbus.py
|
||||||
homeassistant/components/*/modbus.py
|
homeassistant/components/*/modbus.py
|
||||||
|
|
||||||
|
@ -127,6 +124,7 @@ omit =
|
||||||
homeassistant/components/binary_sensor/concord232.py
|
homeassistant/components/binary_sensor/concord232.py
|
||||||
homeassistant/components/binary_sensor/rest.py
|
homeassistant/components/binary_sensor/rest.py
|
||||||
homeassistant/components/browser.py
|
homeassistant/components/browser.py
|
||||||
|
homeassistant/components/camera/amcrest.py
|
||||||
homeassistant/components/camera/bloomsky.py
|
homeassistant/components/camera/bloomsky.py
|
||||||
homeassistant/components/camera/foscam.py
|
homeassistant/components/camera/foscam.py
|
||||||
homeassistant/components/camera/mjpeg.py
|
homeassistant/components/camera/mjpeg.py
|
||||||
|
@ -152,6 +150,7 @@ omit =
|
||||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||||
homeassistant/components/device_tracker/cisco_ios.py
|
homeassistant/components/device_tracker/cisco_ios.py
|
||||||
homeassistant/components/device_tracker/fritz.py
|
homeassistant/components/device_tracker/fritz.py
|
||||||
|
homeassistant/components/device_tracker/gpslogger.py
|
||||||
homeassistant/components/device_tracker/icloud.py
|
homeassistant/components/device_tracker/icloud.py
|
||||||
homeassistant/components/device_tracker/luci.py
|
homeassistant/components/device_tracker/luci.py
|
||||||
homeassistant/components/device_tracker/netgear.py
|
homeassistant/components/device_tracker/netgear.py
|
||||||
|
@ -188,7 +187,9 @@ omit =
|
||||||
homeassistant/components/media_player/cast.py
|
homeassistant/components/media_player/cast.py
|
||||||
homeassistant/components/media_player/cmus.py
|
homeassistant/components/media_player/cmus.py
|
||||||
homeassistant/components/media_player/denon.py
|
homeassistant/components/media_player/denon.py
|
||||||
|
homeassistant/components/media_player/denonavr.py
|
||||||
homeassistant/components/media_player/directv.py
|
homeassistant/components/media_player/directv.py
|
||||||
|
homeassistant/components/media_player/dunehd.py
|
||||||
homeassistant/components/media_player/emby.py
|
homeassistant/components/media_player/emby.py
|
||||||
homeassistant/components/media_player/firetv.py
|
homeassistant/components/media_player/firetv.py
|
||||||
homeassistant/components/media_player/gpmdp.py
|
homeassistant/components/media_player/gpmdp.py
|
||||||
|
@ -240,6 +241,7 @@ omit =
|
||||||
homeassistant/components/notify/xmpp.py
|
homeassistant/components/notify/xmpp.py
|
||||||
homeassistant/components/nuimo_controller.py
|
homeassistant/components/nuimo_controller.py
|
||||||
homeassistant/components/openalpr.py
|
homeassistant/components/openalpr.py
|
||||||
|
homeassistant/components/remote/harmony.py
|
||||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||||
homeassistant/components/sensor/arest.py
|
homeassistant/components/sensor/arest.py
|
||||||
homeassistant/components/sensor/arwn.py
|
homeassistant/components/sensor/arwn.py
|
||||||
|
@ -279,6 +281,7 @@ omit =
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
homeassistant/components/sensor/mqtt_room.py
|
homeassistant/components/sensor/mqtt_room.py
|
||||||
homeassistant/components/sensor/neurio_energy.py
|
homeassistant/components/sensor/neurio_energy.py
|
||||||
|
homeassistant/components/sensor/nut.py
|
||||||
homeassistant/components/sensor/nzbget.py
|
homeassistant/components/sensor/nzbget.py
|
||||||
homeassistant/components/sensor/ohmconnect.py
|
homeassistant/components/sensor/ohmconnect.py
|
||||||
homeassistant/components/sensor/onewire.py
|
homeassistant/components/sensor/onewire.py
|
||||||
|
@ -291,6 +294,7 @@ omit =
|
||||||
homeassistant/components/sensor/scrape.py
|
homeassistant/components/sensor/scrape.py
|
||||||
homeassistant/components/sensor/serial_pm.py
|
homeassistant/components/sensor/serial_pm.py
|
||||||
homeassistant/components/sensor/snmp.py
|
homeassistant/components/sensor/snmp.py
|
||||||
|
homeassistant/components/sensor/sonarr.py
|
||||||
homeassistant/components/sensor/speedtest.py
|
homeassistant/components/sensor/speedtest.py
|
||||||
homeassistant/components/sensor/steam_online.py
|
homeassistant/components/sensor/steam_online.py
|
||||||
homeassistant/components/sensor/supervisord.py
|
homeassistant/components/sensor/supervisord.py
|
||||||
|
@ -306,14 +310,17 @@ omit =
|
||||||
homeassistant/components/sensor/twitch.py
|
homeassistant/components/sensor/twitch.py
|
||||||
homeassistant/components/sensor/uber.py
|
homeassistant/components/sensor/uber.py
|
||||||
homeassistant/components/sensor/vasttrafik.py
|
homeassistant/components/sensor/vasttrafik.py
|
||||||
|
homeassistant/components/sensor/waqi.py
|
||||||
homeassistant/components/sensor/xbox_live.py
|
homeassistant/components/sensor/xbox_live.py
|
||||||
homeassistant/components/sensor/yweather.py
|
homeassistant/components/sensor/yweather.py
|
||||||
|
homeassistant/components/sensor/waqi.py
|
||||||
homeassistant/components/switch/acer_projector.py
|
homeassistant/components/switch/acer_projector.py
|
||||||
homeassistant/components/switch/anel_pwrctrl.py
|
homeassistant/components/switch/anel_pwrctrl.py
|
||||||
homeassistant/components/switch/arest.py
|
homeassistant/components/switch/arest.py
|
||||||
homeassistant/components/switch/dlink.py
|
homeassistant/components/switch/dlink.py
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
homeassistant/components/switch/hikvisioncam.py
|
homeassistant/components/switch/hikvisioncam.py
|
||||||
|
homeassistant/components/switch/hook.py
|
||||||
homeassistant/components/switch/mystrom.py
|
homeassistant/components/switch/mystrom.py
|
||||||
homeassistant/components/switch/netio.py
|
homeassistant/components/switch/netio.py
|
||||||
homeassistant/components/switch/orvibo.py
|
homeassistant/components/switch/orvibo.py
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -62,6 +62,7 @@ pip-log.txt
|
||||||
.coverage
|
.coverage
|
||||||
.tox
|
.tox
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
|
|
@ -365,6 +365,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||||
Dynamically loads required components and its dependencies.
|
Dynamically loads required components and its dependencies.
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
|
hass.async_track_tasks()
|
||||||
setup_lock = hass.data.get('setup_lock')
|
setup_lock = hass.data.get('setup_lock')
|
||||||
if setup_lock is None:
|
if setup_lock is None:
|
||||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||||
|
@ -427,6 +428,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||||
|
|
||||||
setup_lock.release()
|
setup_lock.release()
|
||||||
|
|
||||||
|
yield from hass.async_stop_track_tasks()
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -117,11 +117,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
self._alarm.arm('home')
|
self._alarm.arm('stay')
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
self._alarm.arm('auto')
|
self._alarm.arm('exit')
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
def alarm_trigger(self, code=None):
|
||||||
"""Alarm trigger command."""
|
"""Alarm trigger command."""
|
||||||
|
|
|
@ -118,7 +118,7 @@ class AlexaIntentsView(HomeAssistantView):
|
||||||
|
|
||||||
def __init__(self, hass, intents):
|
def __init__(self, hass, intents):
|
||||||
"""Initialize Alexa view."""
|
"""Initialize Alexa view."""
|
||||||
super().__init__(hass)
|
super().__init__()
|
||||||
|
|
||||||
intents = copy.deepcopy(intents)
|
intents = copy.deepcopy(intents)
|
||||||
template.attach(hass, intents)
|
template.attach(hass, intents)
|
||||||
|
@ -150,7 +150,7 @@ class AlexaIntentsView(HomeAssistantView):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
intent = req.get('intent')
|
intent = req.get('intent')
|
||||||
response = AlexaResponse(self.hass, intent)
|
response = AlexaResponse(request.app['hass'], intent)
|
||||||
|
|
||||||
if req_type == 'LaunchRequest':
|
if req_type == 'LaunchRequest':
|
||||||
response.add_speech(
|
response.add_speech(
|
||||||
|
@ -282,7 +282,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||||
|
|
||||||
def __init__(self, hass, flash_briefings):
|
def __init__(self, hass, flash_briefings):
|
||||||
"""Initialize Alexa view."""
|
"""Initialize Alexa view."""
|
||||||
super().__init__(hass)
|
super().__init__()
|
||||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||||
template.attach(hass, self.flash_briefings)
|
template.attach(hass, self.flash_briefings)
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,10 @@ class APIEventStream(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Provide a streaming interface for the event bus."""
|
"""Provide a streaming interface for the event bus."""
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
hass = request.app['hass']
|
||||||
stop_obj = object()
|
stop_obj = object()
|
||||||
to_write = asyncio.Queue(loop=self.hass.loop)
|
to_write = asyncio.Queue(loop=hass.loop)
|
||||||
|
|
||||||
restrict = request.GET.get('restrict')
|
restrict = request.GET.get('restrict')
|
||||||
if restrict:
|
if restrict:
|
||||||
|
@ -106,7 +108,7 @@ class APIEventStream(HomeAssistantView):
|
||||||
response.content_type = 'text/event-stream'
|
response.content_type = 'text/event-stream'
|
||||||
yield from response.prepare(request)
|
yield from response.prepare(request)
|
||||||
|
|
||||||
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
|
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||||
|
@ -117,7 +119,7 @@ class APIEventStream(HomeAssistantView):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||||
loop=self.hass.loop):
|
loop=hass.loop):
|
||||||
payload = yield from to_write.get()
|
payload = yield from to_write.get()
|
||||||
|
|
||||||
if payload is stop_obj:
|
if payload is stop_obj:
|
||||||
|
@ -145,7 +147,7 @@ class APIConfigView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current configuration."""
|
"""Get current configuration."""
|
||||||
return self.json(self.hass.config.as_dict())
|
return self.json(request.app['hass'].config.as_dict())
|
||||||
|
|
||||||
|
|
||||||
class APIDiscoveryView(HomeAssistantView):
|
class APIDiscoveryView(HomeAssistantView):
|
||||||
|
@ -158,10 +160,11 @@ class APIDiscoveryView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get discovery info."""
|
"""Get discovery info."""
|
||||||
needs_auth = self.hass.config.api.api_password is not None
|
hass = request.app['hass']
|
||||||
|
needs_auth = hass.config.api.api_password is not None
|
||||||
return self.json({
|
return self.json({
|
||||||
'base_url': self.hass.config.api.base_url,
|
'base_url': hass.config.api.base_url,
|
||||||
'location_name': self.hass.config.location_name,
|
'location_name': hass.config.location_name,
|
||||||
'requires_api_password': needs_auth,
|
'requires_api_password': needs_auth,
|
||||||
'version': __version__
|
'version': __version__
|
||||||
})
|
})
|
||||||
|
@ -176,7 +179,7 @@ class APIStatesView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current states."""
|
"""Get current states."""
|
||||||
return self.json(self.hass.states.async_all())
|
return self.json(request.app['hass'].states.async_all())
|
||||||
|
|
||||||
|
|
||||||
class APIEntityStateView(HomeAssistantView):
|
class APIEntityStateView(HomeAssistantView):
|
||||||
|
@ -188,7 +191,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request, entity_id):
|
def get(self, request, entity_id):
|
||||||
"""Retrieve state of entity."""
|
"""Retrieve state of entity."""
|
||||||
state = self.hass.states.get(entity_id)
|
state = request.app['hass'].states.get(entity_id)
|
||||||
if state:
|
if state:
|
||||||
return self.json(state)
|
return self.json(state)
|
||||||
else:
|
else:
|
||||||
|
@ -197,6 +200,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request, entity_id):
|
def post(self, request, entity_id):
|
||||||
"""Update state of entity."""
|
"""Update state of entity."""
|
||||||
|
hass = request.app['hass']
|
||||||
try:
|
try:
|
||||||
data = yield from request.json()
|
data = yield from request.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -211,15 +215,14 @@ class APIEntityStateView(HomeAssistantView):
|
||||||
attributes = data.get('attributes')
|
attributes = data.get('attributes')
|
||||||
force_update = data.get('force_update', False)
|
force_update = data.get('force_update', False)
|
||||||
|
|
||||||
is_new_state = self.hass.states.get(entity_id) is None
|
is_new_state = hass.states.get(entity_id) is None
|
||||||
|
|
||||||
# Write state
|
# Write state
|
||||||
self.hass.states.async_set(entity_id, new_state, attributes,
|
hass.states.async_set(entity_id, new_state, attributes, force_update)
|
||||||
force_update)
|
|
||||||
|
|
||||||
# Read the state back for our response
|
# Read the state back for our response
|
||||||
status_code = HTTP_CREATED if is_new_state else 200
|
status_code = HTTP_CREATED if is_new_state else 200
|
||||||
resp = self.json(self.hass.states.get(entity_id), status_code)
|
resp = self.json(hass.states.get(entity_id), status_code)
|
||||||
|
|
||||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||||
|
|
||||||
|
@ -228,7 +231,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def delete(self, request, entity_id):
|
def delete(self, request, entity_id):
|
||||||
"""Remove entity."""
|
"""Remove entity."""
|
||||||
if self.hass.states.async_remove(entity_id):
|
if request.app['hass'].states.async_remove(entity_id):
|
||||||
return self.json_message('Entity removed')
|
return self.json_message('Entity removed')
|
||||||
else:
|
else:
|
||||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
|
@ -243,7 +246,7 @@ class APIEventListenersView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get event listeners."""
|
"""Get event listeners."""
|
||||||
return self.json(async_events_json(self.hass))
|
return self.json(async_events_json(request.app['hass']))
|
||||||
|
|
||||||
|
|
||||||
class APIEventView(HomeAssistantView):
|
class APIEventView(HomeAssistantView):
|
||||||
|
@ -271,7 +274,8 @@ class APIEventView(HomeAssistantView):
|
||||||
if state:
|
if state:
|
||||||
event_data[key] = state
|
event_data[key] = state
|
||||||
|
|
||||||
self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote)
|
request.app['hass'].bus.async_fire(event_type, event_data,
|
||||||
|
ha.EventOrigin.remote)
|
||||||
|
|
||||||
return self.json_message("Event {} fired.".format(event_type))
|
return self.json_message("Event {} fired.".format(event_type))
|
||||||
|
|
||||||
|
@ -285,7 +289,7 @@ class APIServicesView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get registered services."""
|
"""Get registered services."""
|
||||||
return self.json(async_services_json(self.hass))
|
return self.json(async_services_json(request.app['hass']))
|
||||||
|
|
||||||
|
|
||||||
class APIDomainServicesView(HomeAssistantView):
|
class APIDomainServicesView(HomeAssistantView):
|
||||||
|
@ -300,12 +304,12 @@ class APIDomainServicesView(HomeAssistantView):
|
||||||
|
|
||||||
Returns a list of changed states.
|
Returns a list of changed states.
|
||||||
"""
|
"""
|
||||||
|
hass = request.app['hass']
|
||||||
body = yield from request.text()
|
body = yield from request.text()
|
||||||
data = json.loads(body) if body else None
|
data = json.loads(body) if body else None
|
||||||
|
|
||||||
with AsyncTrackStates(self.hass) as changed_states:
|
with AsyncTrackStates(hass) as changed_states:
|
||||||
yield from self.hass.services.async_call(domain, service, data,
|
yield from hass.services.async_call(domain, service, data, True)
|
||||||
True)
|
|
||||||
|
|
||||||
return self.json(changed_states)
|
return self.json(changed_states)
|
||||||
|
|
||||||
|
@ -320,6 +324,7 @@ class APIEventForwardingView(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Setup an event forwarder."""
|
"""Setup an event forwarder."""
|
||||||
|
hass = request.app['hass']
|
||||||
try:
|
try:
|
||||||
data = yield from request.json()
|
data = yield from request.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -340,14 +345,14 @@ class APIEventForwardingView(HomeAssistantView):
|
||||||
|
|
||||||
api = rem.API(host, api_password, port)
|
api = rem.API(host, api_password, port)
|
||||||
|
|
||||||
valid = yield from self.hass.loop.run_in_executor(
|
valid = yield from hass.loop.run_in_executor(
|
||||||
None, api.validate_api)
|
None, api.validate_api)
|
||||||
if not valid:
|
if not valid:
|
||||||
return self.json_message("Unable to validate API.",
|
return self.json_message("Unable to validate API.",
|
||||||
HTTP_UNPROCESSABLE_ENTITY)
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
if self.event_forwarder is None:
|
if self.event_forwarder is None:
|
||||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
self.event_forwarder = rem.EventForwarder(hass)
|
||||||
|
|
||||||
self.event_forwarder.async_connect(api)
|
self.event_forwarder.async_connect(api)
|
||||||
|
|
||||||
|
@ -389,7 +394,7 @@ class APIComponentsView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current loaded components."""
|
"""Get current loaded components."""
|
||||||
return self.json(self.hass.config.components)
|
return self.json(request.app['hass'].config.components)
|
||||||
|
|
||||||
|
|
||||||
class APIErrorLogView(HomeAssistantView):
|
class APIErrorLogView(HomeAssistantView):
|
||||||
|
@ -402,7 +407,7 @@ class APIErrorLogView(HomeAssistantView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Serve error log."""
|
"""Serve error log."""
|
||||||
resp = yield from self.file(
|
resp = yield from self.file(
|
||||||
request, self.hass.config.path(ERROR_LOG_FILENAME))
|
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@ -417,7 +422,7 @@ class APITemplateView(HomeAssistantView):
|
||||||
"""Render a template."""
|
"""Render a template."""
|
||||||
try:
|
try:
|
||||||
data = yield from request.json()
|
data = yield from request.json()
|
||||||
tpl = template.Template(data['template'], self.hass)
|
tpl = template.Template(data['template'], request.app['hass'])
|
||||||
return tpl.async_render(data.get('variables'))
|
return tpl.async_render(data.get('variables'))
|
||||||
except (ValueError, TemplateError) as ex:
|
except (ValueError, TemplateError) as ex:
|
||||||
return self.json_message('Error rendering template: {}'.format(ex),
|
return self.json_message('Error rendering template: {}'.format(ex),
|
||||||
|
|
|
@ -11,22 +11,34 @@ import voluptuous as vol
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.helpers.event import track_point_in_utc_time
|
||||||
|
|
||||||
DEPENDENCIES = ['litejet']
|
DEPENDENCIES = ['litejet']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_NUMBER = 'number'
|
CONF_NUMBER = 'number'
|
||||||
|
CONF_HELD_MORE_THAN = 'held_more_than'
|
||||||
|
CONF_HELD_LESS_THAN = 'held_less_than'
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): 'litejet',
|
vol.Required(CONF_PLATFORM): 'litejet',
|
||||||
vol.Required(CONF_NUMBER): cv.positive_int
|
vol.Required(CONF_NUMBER): cv.positive_int,
|
||||||
|
vol.Optional(CONF_HELD_MORE_THAN):
|
||||||
|
vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
|
vol.Optional(CONF_HELD_LESS_THAN):
|
||||||
|
vol.All(cv.time_period, cv.positive_timedelta)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def async_trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
number = config.get(CONF_NUMBER)
|
number = config.get(CONF_NUMBER)
|
||||||
|
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||||
|
held_less_than = config.get(CONF_HELD_LESS_THAN)
|
||||||
|
pressed_time = None
|
||||||
|
cancel_pressed_more_than = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
|
@ -34,8 +46,53 @@ def async_trigger(hass, config, action):
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
CONF_PLATFORM: 'litejet',
|
CONF_PLATFORM: 'litejet',
|
||||||
CONF_NUMBER: number
|
CONF_NUMBER: number,
|
||||||
|
CONF_HELD_MORE_THAN: held_more_than,
|
||||||
|
CONF_HELD_LESS_THAN: held_less_than
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
hass.data['litejet_system'].on_switch_released(number, call_action)
|
# held_more_than and held_less_than: trigger on released (if in time range)
|
||||||
|
# held_more_than: trigger after pressed with calculation
|
||||||
|
# held_less_than: trigger on released with calculation
|
||||||
|
# neither: trigger on pressed
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def pressed_more_than_satisfied(now):
|
||||||
|
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
|
||||||
|
call_action()
|
||||||
|
|
||||||
|
def pressed():
|
||||||
|
"""Handle the press of the LiteJet switch's button."""
|
||||||
|
nonlocal cancel_pressed_more_than, pressed_time
|
||||||
|
nonlocal held_less_than, held_more_than
|
||||||
|
pressed_time = dt_util.utcnow()
|
||||||
|
if held_more_than is None and held_less_than is None:
|
||||||
|
call_action()
|
||||||
|
if held_more_than is not None and held_less_than is None:
|
||||||
|
cancel_pressed_more_than = track_point_in_utc_time(
|
||||||
|
hass,
|
||||||
|
pressed_more_than_satisfied,
|
||||||
|
dt_util.utcnow() + held_more_than)
|
||||||
|
|
||||||
|
def released():
|
||||||
|
"""Handle the release of the LiteJet switch's button."""
|
||||||
|
nonlocal cancel_pressed_more_than, pressed_time
|
||||||
|
nonlocal held_less_than, held_more_than
|
||||||
|
# pylint: disable=not-callable
|
||||||
|
if cancel_pressed_more_than is not None:
|
||||||
|
cancel_pressed_more_than()
|
||||||
|
cancel_pressed_more_than = None
|
||||||
|
held_time = dt_util.utcnow() - pressed_time
|
||||||
|
if held_less_than is not None and held_time < held_less_than:
|
||||||
|
if held_more_than is None or held_time > held_more_than:
|
||||||
|
call_action()
|
||||||
|
|
||||||
|
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||||
|
hass.data['litejet_system'].on_switch_released(number, released)
|
||||||
|
|
||||||
|
def async_remove():
|
||||||
|
"""Remove all subscriptions used for this trigger."""
|
||||||
|
return
|
||||||
|
|
||||||
|
return async_remove
|
||||||
|
|
|
@ -4,6 +4,7 @@ Component to interface with binary sensors.
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor/
|
https://home-assistant.io/components/binary_sensor/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -39,13 +40,13 @@ SENSOR_CLASSES = [
|
||||||
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
|
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
"""Track states and offer events for binary sensors."""
|
"""Track states and offer events for binary sensors."""
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
component.setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor.command_line/
|
https://home-assistant.io/components/binary_sensor.command_line/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
|
||||||
DEFAULT_PAYLOAD_ON = 'ON'
|
DEFAULT_PAYLOAD_ON = 'ON'
|
||||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
SCAN_INTERVAL = 60
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_COMMAND): cv.string,
|
vol.Required(CONF_COMMAND): cv.string,
|
||||||
|
|
|
@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.homematic/
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
import homeassistant.components.homematic as homematic
|
from homeassistant.components.homematic import HMDevice
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -32,14 +33,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
homematic = get_component("homematic")
|
||||||
return homematic.setup_hmdevice_discovery_helper(
|
return homematic.setup_hmdevice_discovery_helper(
|
||||||
|
hass,
|
||||||
HMBinarySensor,
|
HMBinarySensor,
|
||||||
discovery_info,
|
discovery_info,
|
||||||
add_callback_devices
|
add_callback_devices
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||||
"""Representation of a binary Homematic device."""
|
"""Representation of a binary Homematic device."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -4,17 +4,34 @@ Support for Nest Thermostat Binary Sensors.
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor.nest/
|
https://home-assistant.io/components/binary_sensor.nest/
|
||||||
"""
|
"""
|
||||||
|
from itertools import chain
|
||||||
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.components.sensor.nest import NestSensor
|
from homeassistant.components.sensor.nest import NestSensor
|
||||||
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
||||||
from homeassistant.components.nest import DATA_NEST
|
from homeassistant.components.nest import (
|
||||||
|
DATA_NEST, is_thermostat, is_camera)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['nest']
|
DEPENDENCIES = ['nest']
|
||||||
BINARY_TYPES = ['fan',
|
|
||||||
|
BINARY_TYPES = ['online']
|
||||||
|
|
||||||
|
CLIMATE_BINARY_TYPES = ['fan',
|
||||||
|
'is_using_emergency_heat',
|
||||||
|
'is_locked',
|
||||||
|
'has_leaf']
|
||||||
|
|
||||||
|
CAMERA_BINARY_TYPES = [
|
||||||
|
'motion_detected',
|
||||||
|
'sound_detected',
|
||||||
|
'person_detected']
|
||||||
|
|
||||||
|
_BINARY_TYPES_DEPRECATED = [
|
||||||
'hvac_ac_state',
|
'hvac_ac_state',
|
||||||
'hvac_aux_heater_state',
|
'hvac_aux_heater_state',
|
||||||
'hvac_heater_state',
|
'hvac_heater_state',
|
||||||
|
@ -22,28 +39,62 @@ BINARY_TYPES = ['fan',
|
||||||
'hvac_heat_x3_state',
|
'hvac_heat_x3_state',
|
||||||
'hvac_alt_heat_state',
|
'hvac_alt_heat_state',
|
||||||
'hvac_alt_heat_x2_state',
|
'hvac_alt_heat_x2_state',
|
||||||
'hvac_emer_heat_state',
|
'hvac_emer_heat_state']
|
||||||
'online']
|
|
||||||
|
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
|
||||||
|
+ CAMERA_BINARY_TYPES
|
||||||
|
_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED = _VALID_BINARY_SENSOR_TYPES \
|
||||||
|
+ _BINARY_TYPES_DEPRECATED
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_SCAN_INTERVAL):
|
vol.Optional(CONF_SCAN_INTERVAL):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||||
vol.All(cv.ensure_list, [vol.In(BINARY_TYPES)]),
|
vol.All(cv.ensure_list,
|
||||||
|
[vol.In(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup Nest binary sensors."""
|
"""Setup Nest binary sensors."""
|
||||||
nest = hass.data[DATA_NEST]
|
nest = hass.data[DATA_NEST]
|
||||||
|
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
|
||||||
|
|
||||||
all_sensors = []
|
for variable in conf:
|
||||||
for structure, device in nest.devices():
|
if variable in _BINARY_TYPES_DEPRECATED:
|
||||||
all_sensors.extend(
|
wstr = (variable + " is no a longer supported "
|
||||||
[NestBinarySensor(structure, device, variable)
|
"monitored_conditions. See "
|
||||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
"https://home-assistant.io/components/binary_sensor.nest/ "
|
||||||
|
"for valid options, or remove monitored_conditions "
|
||||||
|
"entirely to get a reasonable default")
|
||||||
|
_LOGGER.error(wstr)
|
||||||
|
|
||||||
add_devices(all_sensors, True)
|
sensors = []
|
||||||
|
device_chain = chain(nest.devices(),
|
||||||
|
nest.protect_devices(),
|
||||||
|
nest.camera_devices())
|
||||||
|
for structure, device in device_chain:
|
||||||
|
sensors += [NestBinarySensor(structure, device, variable)
|
||||||
|
for variable in conf
|
||||||
|
if variable in BINARY_TYPES]
|
||||||
|
sensors += [NestBinarySensor(structure, device, variable)
|
||||||
|
for variable in conf
|
||||||
|
if variable in CLIMATE_BINARY_TYPES
|
||||||
|
and is_thermostat(device)]
|
||||||
|
|
||||||
|
if is_camera(device):
|
||||||
|
sensors += [NestBinarySensor(structure, device, variable)
|
||||||
|
for variable in conf
|
||||||
|
if variable in CAMERA_BINARY_TYPES]
|
||||||
|
for activity_zone in device.activity_zones:
|
||||||
|
sensors += [NestActivityZoneSensor(structure,
|
||||||
|
device,
|
||||||
|
activity_zone)]
|
||||||
|
|
||||||
|
add_devices(sensors, True)
|
||||||
|
|
||||||
|
|
||||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||||
|
@ -57,3 +108,21 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Retrieve latest state."""
|
"""Retrieve latest state."""
|
||||||
self._state = bool(getattr(self.device, self.variable))
|
self._state = bool(getattr(self.device, self.variable))
|
||||||
|
|
||||||
|
|
||||||
|
class NestActivityZoneSensor(NestBinarySensor):
|
||||||
|
"""Represents a Nest binary sensor for activity in a zone."""
|
||||||
|
|
||||||
|
def __init__(self, structure, device, zone):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super(NestActivityZoneSensor, self).__init__(structure, device, None)
|
||||||
|
self.zone = zone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the nest, if any."""
|
||||||
|
return "{} {} activity".format(self._name, self.zone.name)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Retrieve latest state."""
|
||||||
|
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)
|
||||||
|
|
128
homeassistant/components/binary_sensor/threshold.py
Normal file
128
homeassistant/components/binary_sensor/threshold.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
Support for monitoring if a sensor value is below/above a threshold.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.threshold/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS,
|
||||||
|
ATTR_ENTITY_ID)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_SENSOR_VALUE = 'sensor_value'
|
||||||
|
ATTR_THRESHOLD = 'threshold'
|
||||||
|
ATTR_TYPE = 'type'
|
||||||
|
|
||||||
|
CONF_LOWER = 'lower'
|
||||||
|
CONF_THRESHOLD = 'threshold'
|
||||||
|
CONF_UPPER = 'upper'
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Threshold'
|
||||||
|
|
||||||
|
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
|
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||||
|
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the Threshold sensor."""
|
||||||
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
threshold = config.get(CONF_THRESHOLD)
|
||||||
|
limit_type = config.get(CONF_TYPE)
|
||||||
|
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||||
|
|
||||||
|
yield from async_add_devices(
|
||||||
|
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||||
|
sensor_class)], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ThresholdSensor(BinarySensorDevice):
|
||||||
|
"""Representation of a Threshold sensor."""
|
||||||
|
|
||||||
|
def __init__(self, hass, entity_id, name, threshold, limit_type,
|
||||||
|
sensor_class):
|
||||||
|
"""Initialize the Threshold sensor."""
|
||||||
|
self._hass = hass
|
||||||
|
self._entity_id = entity_id
|
||||||
|
self.is_upper = limit_type == 'upper'
|
||||||
|
self._name = name
|
||||||
|
self._threshold = threshold
|
||||||
|
self._sensor_class = sensor_class
|
||||||
|
self._deviation = False
|
||||||
|
self.sensor_value = 0
|
||||||
|
|
||||||
|
@callback
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
def async_threshold_sensor_state_listener(
|
||||||
|
entity, old_state, new_state):
|
||||||
|
"""Called when the sensor changes state."""
|
||||||
|
if new_state.state == STATE_UNKNOWN:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sensor_value = float(new_state.state)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error("State is not numerical")
|
||||||
|
|
||||||
|
hass.async_add_job(self.async_update_ha_state, True)
|
||||||
|
|
||||||
|
async_track_state_change(
|
||||||
|
hass, entity_id, async_threshold_sensor_state_listener)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if sensor is on."""
|
||||||
|
return self._deviation
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensor_class(self):
|
||||||
|
"""Return the sensor class of the sensor."""
|
||||||
|
return self._sensor_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return the state attributes of the sensor."""
|
||||||
|
return {
|
||||||
|
ATTR_ENTITY_ID: self._entity_id,
|
||||||
|
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||||
|
ATTR_THRESHOLD: self._threshold,
|
||||||
|
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
|
||||||
|
}
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
|
"""Get the latest data and updates the states."""
|
||||||
|
if self.is_upper:
|
||||||
|
self._deviation = bool(self.sensor_value > self._threshold)
|
||||||
|
else:
|
||||||
|
self._deviation = bool(self.sensor_value < self._threshold)
|
|
@ -4,8 +4,6 @@ Support for Wink binary sensors.
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
at https://home-assistant.io/components/binary_sensor.wink/
|
at https://home-assistant.io/components/binary_sensor.wink/
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.sensor.wink import WinkDevice
|
from homeassistant.components.sensor.wink import WinkDevice
|
||||||
|
@ -34,38 +32,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
|
||||||
for sensor in pywink.get_sensors():
|
for sensor in pywink.get_sensors():
|
||||||
if sensor.capability() in SENSOR_TYPES:
|
if sensor.capability() in SENSOR_TYPES:
|
||||||
add_devices([WinkBinarySensorDevice(sensor)])
|
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||||
|
|
||||||
for key in pywink.get_keys():
|
for key in pywink.get_keys():
|
||||||
add_devices([WinkBinarySensorDevice(key)])
|
add_devices([WinkBinarySensorDevice(key, hass)])
|
||||||
|
|
||||||
for sensor in pywink.get_smoke_and_co_detectors():
|
for sensor in pywink.get_smoke_and_co_detectors():
|
||||||
add_devices([WinkBinarySensorDevice(sensor)])
|
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||||
|
|
||||||
|
|
||||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||||
"""Representation of a Wink binary sensor."""
|
"""Representation of a Wink binary sensor."""
|
||||||
|
|
||||||
def __init__(self, wink):
|
def __init__(self, wink, hass):
|
||||||
"""Initialize the Wink binary sensor."""
|
"""Initialize the Wink binary sensor."""
|
||||||
super().__init__(wink)
|
super().__init__(wink, hass)
|
||||||
wink = get_component('wink')
|
wink = get_component('wink')
|
||||||
self._unit_of_measurement = self.wink.UNIT
|
self._unit_of_measurement = self.wink.UNIT
|
||||||
self.capability = self.wink.capability()
|
self.capability = self.wink.capability()
|
||||||
|
|
||||||
def _pubnub_update(self, message, channel):
|
|
||||||
try:
|
|
||||||
if 'data' in message:
|
|
||||||
json_data = json.dumps(message.get('data'))
|
|
||||||
else:
|
|
||||||
json_data = message
|
|
||||||
self.wink.pubnub_update(json.loads(json_data))
|
|
||||||
self.update_ha_state()
|
|
||||||
except (AttributeError, KeyError):
|
|
||||||
error = "Pubnub returned invalid json for " + self.name
|
|
||||||
logging.getLogger(__name__).error(error)
|
|
||||||
self.update_ha_state(True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
|
|
|
@ -13,7 +13,7 @@ from aiohttp import web
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||||
|
|
||||||
DOMAIN = 'camera'
|
DOMAIN = 'camera'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
@ -33,8 +33,8 @@ def async_setup(hass, config):
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
hass.http.register_view(CameraImageView(hass, component.entities))
|
hass.http.register_view(CameraImageView(component.entities))
|
||||||
hass.http.register_view(CameraMjpegStream(hass, component.entities))
|
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
return True
|
return True
|
||||||
|
@ -165,9 +165,8 @@ class CameraView(HomeAssistantView):
|
||||||
|
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, hass, entities):
|
def __init__(self, entities):
|
||||||
"""Initialize a basic camera view."""
|
"""Initialize a basic camera view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -178,7 +177,7 @@ class CameraView(HomeAssistantView):
|
||||||
if camera is None:
|
if camera is None:
|
||||||
return web.Response(status=404)
|
return web.Response(status=404)
|
||||||
|
|
||||||
authenticated = (request.authenticated or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.GET.get('token') == camera.access_token)
|
request.GET.get('token') == camera.access_token)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
|
|
79
homeassistant/components/camera/amcrest.py
Normal file
79
homeassistant/components/camera/amcrest.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
"""
|
||||||
|
This component provides basic support for Amcrest IP cameras.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/camera.amcrest/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.loader as loader
|
||||||
|
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['amcrest==1.0.0']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_NAME = 'Amcrest Camera'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'amcrest_notification'
|
||||||
|
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up an Amcrest IP Camera."""
|
||||||
|
from amcrest import AmcrestCamera
|
||||||
|
data = AmcrestCamera(
|
||||||
|
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||||
|
config.get(CONF_USERNAME), config.get(CONF_PASSWORD))
|
||||||
|
|
||||||
|
persistent_notification = loader.get_component('persistent_notification')
|
||||||
|
try:
|
||||||
|
data.camera.current_time
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
except Exception as ex:
|
||||||
|
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'Error: {}<br />'
|
||||||
|
'You will need to restart hass after fixing.'
|
||||||
|
''.format(ex),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices([AmcrestCam(config, data)])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AmcrestCam(Camera):
|
||||||
|
"""An implementation of an Amcrest IP camera."""
|
||||||
|
|
||||||
|
def __init__(self, device_info, data):
|
||||||
|
"""Initialize an Amcrest camera."""
|
||||||
|
super(AmcrestCam, self).__init__()
|
||||||
|
self._name = device_info.get(CONF_NAME)
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
"""Return a still image reponse from the camera."""
|
||||||
|
# Send the request to snap a picture and return raw jpg data
|
||||||
|
response = self._data.camera.snapshot()
|
||||||
|
return response.data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of this camera."""
|
||||||
|
return self._name
|
|
@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
|
@ -96,8 +97,7 @@ class GenericCamera(Camera):
|
||||||
def fetch():
|
def fetch():
|
||||||
"""Read image from a URL."""
|
"""Read image from a URL."""
|
||||||
try:
|
try:
|
||||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
response = requests.get(url, timeout=10, auth=self._auth)
|
||||||
response = requests.get(url, **kwargs)
|
|
||||||
return response.content
|
return response.content
|
||||||
except requests.exceptions.RequestException as error:
|
except requests.exceptions.RequestException as error:
|
||||||
_LOGGER.error('Error getting camera image: %s', error)
|
_LOGGER.error('Error getting camera image: %s', error)
|
||||||
|
@ -107,12 +107,13 @@ class GenericCamera(Camera):
|
||||||
None, fetch)
|
None, fetch)
|
||||||
# async
|
# async
|
||||||
else:
|
else:
|
||||||
|
response = None
|
||||||
try:
|
try:
|
||||||
|
websession = async_get_clientsession(self.hass)
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
response = yield from self.hass.websession.get(
|
response = yield from websession.get(
|
||||||
url, auth=self._auth)
|
url, auth=self._auth)
|
||||||
self._last_image = yield from response.read()
|
self._last_image = yield from response.read()
|
||||||
yield from response.release()
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error('Timeout getting camera image')
|
_LOGGER.error('Timeout getting camera image')
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
@ -120,6 +121,9 @@ class GenericCamera(Camera):
|
||||||
aiohttp.errors.ClientDisconnectedError) as err:
|
aiohttp.errors.ClientDisconnectedError) as err:
|
||||||
_LOGGER.error('Error getting new camera image: %s', err)
|
_LOGGER.error('Error getting new camera image: %s', err)
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
finally:
|
||||||
|
if response is not None:
|
||||||
|
self.hass.async_add_job(response.release())
|
||||||
|
|
||||||
self._last_url = url
|
self._last_url = url
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
|
@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -101,28 +102,32 @@ class MjpegCamera(Camera):
|
||||||
return
|
return
|
||||||
|
|
||||||
# connect to stream
|
# connect to stream
|
||||||
|
websession = async_get_clientsession(self.hass)
|
||||||
|
stream = None
|
||||||
|
response = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
stream = yield from self.hass.websession.get(
|
stream = yield from websession.get(self._mjpeg_url,
|
||||||
self._mjpeg_url,
|
auth=self._auth)
|
||||||
auth=self._auth
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
raise HTTPGatewayTimeout()
|
|
||||||
|
|
||||||
response = web.StreamResponse()
|
response = web.StreamResponse()
|
||||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||||
|
|
||||||
yield from response.prepare(request)
|
yield from response.prepare(request)
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
while True:
|
||||||
data = yield from stream.content.read(102400)
|
data = yield from stream.content.read(102400)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
response.write(data)
|
response.write(data)
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise HTTPGatewayTimeout()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
if stream is not None:
|
||||||
self.hass.async_add_job(stream.release())
|
self.hass.async_add_job(stream.release())
|
||||||
|
if response is not None:
|
||||||
yield from response.write_eof()
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
108
homeassistant/components/camera/nest.py
Normal file
108
homeassistant/components/camera/nest.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
"""
|
||||||
|
Support for Nest Cameras.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/camera.nest/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import homeassistant.components.nest as nest
|
||||||
|
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['nest']
|
||||||
|
|
||||||
|
NEST_BRAND = 'Nest'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up a Nest Cam."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
camera_devices = hass.data[nest.DATA_NEST].camera_devices()
|
||||||
|
cameras = [NestCamera(structure, device)
|
||||||
|
for structure, device in camera_devices]
|
||||||
|
add_devices(cameras, True)
|
||||||
|
|
||||||
|
|
||||||
|
class NestCamera(Camera):
|
||||||
|
"""Representation of a Nest Camera."""
|
||||||
|
|
||||||
|
def __init__(self, structure, device):
|
||||||
|
"""Initialize a Nest Camera."""
|
||||||
|
super(NestCamera, self).__init__()
|
||||||
|
self.structure = structure
|
||||||
|
self.device = device
|
||||||
|
self._location = None
|
||||||
|
self._name = None
|
||||||
|
self._is_online = None
|
||||||
|
self._is_streaming = None
|
||||||
|
self._is_video_history_enabled = False
|
||||||
|
# Default to non-NestAware subscribed, but will be fixed during update
|
||||||
|
self._time_between_snapshots = timedelta(seconds=30)
|
||||||
|
self._last_image = None
|
||||||
|
self._next_snapshot_at = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the nest, if any."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Nest camera should poll periodically."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_recording(self):
|
||||||
|
"""Return true if the device is recording."""
|
||||||
|
return self._is_streaming
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brand(self):
|
||||||
|
"""Return the brand of the camera."""
|
||||||
|
return NEST_BRAND
|
||||||
|
|
||||||
|
# This doesn't seem to be getting called regularly, for some reason
|
||||||
|
def update(self):
|
||||||
|
"""Cache value from Python-nest."""
|
||||||
|
self._location = self.device.where
|
||||||
|
self._name = self.device.name
|
||||||
|
self._is_online = self.device.is_online
|
||||||
|
self._is_streaming = self.device.is_streaming
|
||||||
|
self._is_video_history_enabled = self.device.is_video_history_enabled
|
||||||
|
|
||||||
|
if self._is_video_history_enabled:
|
||||||
|
# NestAware allowed 10/min
|
||||||
|
self._time_between_snapshots = timedelta(seconds=6)
|
||||||
|
else:
|
||||||
|
# Otherwise, 2/min
|
||||||
|
self._time_between_snapshots = timedelta(seconds=30)
|
||||||
|
|
||||||
|
def _ready_for_snapshot(self, now):
|
||||||
|
return (self._next_snapshot_at is None or
|
||||||
|
now > self._next_snapshot_at)
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
"""Return a still image response from the camera."""
|
||||||
|
now = utcnow()
|
||||||
|
if self._ready_for_snapshot(now):
|
||||||
|
url = self.device.snapshot_url
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
except requests.exceptions.RequestException as error:
|
||||||
|
_LOGGER.error("Error getting camera image: %s", error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._next_snapshot_at = now + self._time_between_snapshots
|
||||||
|
self._last_image = response.content
|
||||||
|
|
||||||
|
return self._last_image
|
|
@ -14,12 +14,13 @@ from aiohttp import web
|
||||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
|
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||||
from homeassistant.components.camera import (
|
from homeassistant.components.camera import (
|
||||||
Camera, PLATFORM_SCHEMA)
|
Camera, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
|
async_get_clientsession, async_create_clientsession)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
|
@ -59,23 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a Synology IP Camera."""
|
"""Setup a Synology IP Camera."""
|
||||||
if not config.get(CONF_VERIFY_SSL):
|
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||||
connector = aiohttp.TCPConnector(verify_ssl=False)
|
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def _async_close_connector(event):
|
|
||||||
"""Close websession on shutdown."""
|
|
||||||
yield from connector.close()
|
|
||||||
|
|
||||||
hass.bus.async_listen_once(
|
|
||||||
EVENT_HOMEASSISTANT_STOP, _async_close_connector)
|
|
||||||
else:
|
|
||||||
connector = hass.websession.connector
|
|
||||||
|
|
||||||
websession_init = aiohttp.ClientSession(
|
|
||||||
loop=hass.loop,
|
|
||||||
connector=connector
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine API to use for authentication
|
# Determine API to use for authentication
|
||||||
syno_api_url = SYNO_API_URL.format(
|
syno_api_url = SYNO_API_URL.format(
|
||||||
|
@ -87,15 +73,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'query': 'SYNO.'
|
'query': 'SYNO.'
|
||||||
}
|
}
|
||||||
|
query_req = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||||
query_req = yield from websession_init.get(
|
query_req = yield from websession_init.get(
|
||||||
syno_api_url,
|
syno_api_url,
|
||||||
params=query_payload
|
params=query_payload
|
||||||
)
|
)
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
||||||
_LOGGER.exception("Error on %s", syno_api_url)
|
|
||||||
return False
|
|
||||||
|
|
||||||
query_resp = yield from query_req.json()
|
query_resp = yield from query_req.json()
|
||||||
auth_path = query_resp['data'][AUTH_API]['path']
|
auth_path = query_resp['data'][AUTH_API]['path']
|
||||||
|
@ -103,7 +87,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||||
|
|
||||||
# cleanup
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
|
_LOGGER.exception("Error on %s", syno_api_url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if query_req is not None:
|
||||||
yield from query_req.release()
|
yield from query_req.release()
|
||||||
|
|
||||||
# Authticate to NAS to get a session id
|
# Authticate to NAS to get a session id
|
||||||
|
@ -118,19 +107,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
syno_auth_url
|
syno_auth_url
|
||||||
)
|
)
|
||||||
|
|
||||||
websession_init.detach()
|
|
||||||
|
|
||||||
# init websession
|
# init websession
|
||||||
websession = aiohttp.ClientSession(
|
websession = async_create_clientsession(
|
||||||
loop=hass.loop, connector=connector, cookies={'id': session_id})
|
hass, verify_ssl, cookies={'id': session_id})
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_close_websession(event):
|
|
||||||
"""Close websession on shutdown."""
|
|
||||||
websession.detach()
|
|
||||||
|
|
||||||
hass.bus.async_listen_once(
|
|
||||||
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
|
||||||
|
|
||||||
# Use SessionID to get cameras in system
|
# Use SessionID to get cameras in system
|
||||||
syno_camera_url = SYNO_API_URL.format(
|
syno_camera_url = SYNO_API_URL.format(
|
||||||
|
@ -190,21 +169,24 @@ def get_session_id(hass, websession, username, password, login_url):
|
||||||
'session': 'SurveillanceStation',
|
'session': 'SurveillanceStation',
|
||||||
'format': 'sid'
|
'format': 'sid'
|
||||||
}
|
}
|
||||||
|
auth_req = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||||
auth_req = yield from websession.get(
|
auth_req = yield from websession.get(
|
||||||
login_url,
|
login_url,
|
||||||
params=auth_payload
|
params=auth_payload
|
||||||
)
|
)
|
||||||
|
auth_resp = yield from auth_req.json()
|
||||||
|
return auth_resp['data']['sid']
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
_LOGGER.exception("Error on %s", login_url)
|
_LOGGER.exception("Error on %s", login_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
auth_resp = yield from auth_req.json()
|
finally:
|
||||||
|
if auth_req is not None:
|
||||||
yield from auth_req.release()
|
yield from auth_req.release()
|
||||||
|
|
||||||
return auth_resp['data']['sid']
|
|
||||||
|
|
||||||
|
|
||||||
class SynologyCamera(Camera):
|
class SynologyCamera(Camera):
|
||||||
"""An implementation of a Synology NAS based IP camera."""
|
"""An implementation of a Synology NAS based IP camera."""
|
||||||
|
@ -271,29 +253,33 @@ class SynologyCamera(Camera):
|
||||||
'cameraId': self._camera_id,
|
'cameraId': self._camera_id,
|
||||||
'format': 'mjpeg'
|
'format': 'mjpeg'
|
||||||
}
|
}
|
||||||
|
stream = None
|
||||||
|
response = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||||
stream = yield from self._websession.get(
|
stream = yield from self._websession.get(
|
||||||
streaming_url,
|
streaming_url,
|
||||||
params=streaming_payload
|
params=streaming_payload
|
||||||
)
|
)
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
||||||
_LOGGER.exception("Error on %s", streaming_url)
|
|
||||||
raise HTTPGatewayTimeout()
|
|
||||||
|
|
||||||
response = web.StreamResponse()
|
response = web.StreamResponse()
|
||||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||||
|
|
||||||
yield from response.prepare(request)
|
yield from response.prepare(request)
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
while True:
|
||||||
data = yield from stream.content.read(102400)
|
data = yield from stream.content.read(102400)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
response.write(data)
|
response.write(data)
|
||||||
|
|
||||||
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
|
_LOGGER.exception("Error on %s", streaming_url)
|
||||||
|
raise HTTPGatewayTimeout()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
if stream is not None:
|
||||||
self.hass.async_add_job(stream.release())
|
self.hass.async_add_job(stream.release())
|
||||||
|
if response is not None:
|
||||||
yield from response.write_eof()
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -58,6 +58,11 @@ ATTR_OPERATION_LIST = "operation_list"
|
||||||
ATTR_SWING_MODE = "swing_mode"
|
ATTR_SWING_MODE = "swing_mode"
|
||||||
ATTR_SWING_LIST = "swing_list"
|
ATTR_SWING_LIST = "swing_list"
|
||||||
|
|
||||||
|
# The degree of precision for each platform
|
||||||
|
PRECISION_WHOLE = 1
|
||||||
|
PRECISION_HALVES = 0.5
|
||||||
|
PRECISION_TENTHS = 0.1
|
||||||
|
|
||||||
CONVERTIBLE_ATTRIBUTE = [
|
CONVERTIBLE_ATTRIBUTE = [
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
ATTR_TARGET_TEMP_LOW,
|
ATTR_TARGET_TEMP_LOW,
|
||||||
|
@ -371,6 +376,14 @@ class ClimateDevice(Entity):
|
||||||
else:
|
else:
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def precision(self):
|
||||||
|
"""Return the precision of the system."""
|
||||||
|
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||||
|
return PRECISION_TENTHS
|
||||||
|
else:
|
||||||
|
return PRECISION_WHOLE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the optional state attributes."""
|
"""Return the optional state attributes."""
|
||||||
|
@ -562,16 +575,18 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
def _convert_for_display(self, temp):
|
def _convert_for_display(self, temp):
|
||||||
"""Convert temperature into preferred units for display purposes."""
|
"""Convert temperature into preferred units for display purposes."""
|
||||||
if temp is None or not isinstance(temp, Number):
|
if (temp is None or not isinstance(temp, Number) or
|
||||||
|
self.temperature_unit == self.unit_of_measurement):
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
value = convert_temperature(temp, self.temperature_unit,
|
value = convert_temperature(temp, self.temperature_unit,
|
||||||
self.unit_of_measurement)
|
self.unit_of_measurement)
|
||||||
|
|
||||||
if self.unit_of_measurement is TEMP_CELSIUS:
|
# Round in the units appropriate
|
||||||
decimal_count = 1
|
if self.precision == PRECISION_HALVES:
|
||||||
|
return round(value * 2) / 2.0
|
||||||
|
elif self.precision == PRECISION_TENTHS:
|
||||||
|
return round(value, 1)
|
||||||
else:
|
else:
|
||||||
# Users of fahrenheit generally expect integer units.
|
# PRECISION_WHOLE as a fall back
|
||||||
decimal_count = 0
|
return round(value)
|
||||||
|
|
||||||
return round(value, decimal_count)
|
|
||||||
|
|
|
@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['switch', 'sensor']
|
DEPENDENCIES = ['switch', 'sensor']
|
||||||
|
|
||||||
TOL_TEMP = 0.3
|
DEFAULT_TOLERANCE = 0.3
|
||||||
|
DEFAULT_NAME = 'Generic Thermostat'
|
||||||
|
|
||||||
CONF_NAME = 'name'
|
CONF_NAME = 'name'
|
||||||
DEFAULT_NAME = 'Generic Thermostat'
|
|
||||||
CONF_HEATER = 'heater'
|
CONF_HEATER = 'heater'
|
||||||
CONF_SENSOR = 'target_sensor'
|
CONF_SENSOR = 'target_sensor'
|
||||||
CONF_MIN_TEMP = 'min_temp'
|
CONF_MIN_TEMP = 'min_temp'
|
||||||
|
@ -32,6 +32,7 @@ CONF_MAX_TEMP = 'max_temp'
|
||||||
CONF_TARGET_TEMP = 'target_temp'
|
CONF_TARGET_TEMP = 'target_temp'
|
||||||
CONF_AC_MODE = 'ac_mode'
|
CONF_AC_MODE = 'ac_mode'
|
||||||
CONF_MIN_DUR = 'min_cycle_duration'
|
CONF_MIN_DUR = 'min_cycle_duration'
|
||||||
|
CONF_TOLERANCE = 'tolerance'
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
@ -42,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -56,23 +58,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
target_temp = config.get(CONF_TARGET_TEMP)
|
target_temp = config.get(CONF_TARGET_TEMP)
|
||||||
ac_mode = config.get(CONF_AC_MODE)
|
ac_mode = config.get(CONF_AC_MODE)
|
||||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||||
|
tolerance = config.get(CONF_TOLERANCE)
|
||||||
|
|
||||||
add_devices([GenericThermostat(
|
add_devices([GenericThermostat(
|
||||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||||
target_temp, ac_mode, min_cycle_duration)])
|
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||||
|
|
||||||
|
|
||||||
class GenericThermostat(ClimateDevice):
|
class GenericThermostat(ClimateDevice):
|
||||||
"""Representation of a GenericThermostat device."""
|
"""Representation of a GenericThermostat device."""
|
||||||
|
|
||||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
|
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||||
|
tolerance):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self.heater_entity_id = heater_entity_id
|
self.heater_entity_id = heater_entity_id
|
||||||
self.ac_mode = ac_mode
|
self.ac_mode = ac_mode
|
||||||
self.min_cycle_duration = min_cycle_duration
|
self.min_cycle_duration = min_cycle_duration
|
||||||
|
self._tolerance = tolerance
|
||||||
|
|
||||||
self._active = False
|
self._active = False
|
||||||
self._cur_temp = None
|
self._cur_temp = None
|
||||||
|
@ -193,7 +198,7 @@ class GenericThermostat(ClimateDevice):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.ac_mode:
|
if self.ac_mode:
|
||||||
too_hot = self._cur_temp - self._target_temp > TOL_TEMP
|
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||||
is_cooling = self._is_device_active
|
is_cooling = self._is_device_active
|
||||||
if too_hot and not is_cooling:
|
if too_hot and not is_cooling:
|
||||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||||
|
@ -202,7 +207,7 @@ class GenericThermostat(ClimateDevice):
|
||||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||||
switch.turn_off(self.hass, self.heater_entity_id)
|
switch.turn_off(self.hass, self.heater_entity_id)
|
||||||
else:
|
else:
|
||||||
too_cold = self._target_temp - self._cur_temp > TOL_TEMP
|
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||||
is_heating = self._is_device_active
|
is_heating = self._is_device_active
|
||||||
|
|
||||||
if too_cold and not is_heating:
|
if too_cold and not is_heating:
|
||||||
|
|
|
@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/climate.homematic/
|
https://home-assistant.io/components/climate.homematic/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.components.homematic as homematic
|
|
||||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||||
|
from homeassistant.components.homematic import HMDevice
|
||||||
from homeassistant.util.temperature import convert
|
from homeassistant.util.temperature import convert
|
||||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
DEPENDENCIES = ['homematic']
|
DEPENDENCIES = ['homematic']
|
||||||
|
|
||||||
|
@ -29,14 +30,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
homematic = get_component("homematic")
|
||||||
return homematic.setup_hmdevice_discovery_helper(
|
return homematic.setup_hmdevice_discovery_helper(
|
||||||
|
hass,
|
||||||
HMThermostat,
|
HMThermostat,
|
||||||
discovery_info,
|
discovery_info,
|
||||||
add_callback_devices
|
add_callback_devices
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
class HMThermostat(HMDevice, ClimateDevice):
|
||||||
"""Representation of a Homematic thermostat."""
|
"""Representation of a Homematic thermostat."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -94,13 +97,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||||
def set_temperature(self, **kwargs):
|
def set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
if not self.available:
|
if not self.available or temperature is None:
|
||||||
return None
|
return None
|
||||||
if temperature is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.current_operation == STATE_AUTO:
|
|
||||||
return self._hmdevice.actionNodeData('MANU_MODE', temperature)
|
|
||||||
self._hmdevice.set_temperature(temperature)
|
self._hmdevice.set_temperature(temperature)
|
||||||
|
|
||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
|
|
|
@ -14,7 +14,8 @@ from homeassistant.components.climate import (
|
||||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||||
ATTR_TEMPERATURE)
|
ATTR_TEMPERATURE)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||||
|
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||||
|
|
||||||
DEPENDENCIES = ['nest']
|
DEPENDENCIES = ['nest']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -24,10 +25,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
STATE_ECO = 'eco'
|
||||||
|
STATE_HEAT_COOL = 'heat-cool'
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Nest thermostat."""
|
"""Setup the Nest thermostat."""
|
||||||
|
_LOGGER.debug("Setting up nest thermostat")
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
temp_unit = hass.config.units.temperature_unit
|
temp_unit = hass.config.units.temperature_unit
|
||||||
|
|
||||||
add_devices(
|
add_devices(
|
||||||
[NestThermostat(structure, device, temp_unit)
|
[NestThermostat(structure, device, temp_unit)
|
||||||
for structure, device in hass.data[DATA_NEST].devices()],
|
for structure, device in hass.data[DATA_NEST].devices()],
|
||||||
|
@ -58,9 +67,9 @@ class NestThermostat(ClimateDevice):
|
||||||
if self.device.can_heat and self.device.can_cool:
|
if self.device.can_heat and self.device.can_cool:
|
||||||
self._operation_list.append(STATE_AUTO)
|
self._operation_list.append(STATE_AUTO)
|
||||||
|
|
||||||
|
self._operation_list.append(STATE_ECO)
|
||||||
|
|
||||||
# feature of device
|
# feature of device
|
||||||
self._has_humidifier = self.device.has_humidifier
|
|
||||||
self._has_dehumidifier = self.device.has_dehumidifier
|
|
||||||
self._has_fan = self.device.has_fan
|
self._has_fan = self.device.has_fan
|
||||||
|
|
||||||
# data attributes
|
# data attributes
|
||||||
|
@ -68,41 +77,24 @@ class NestThermostat(ClimateDevice):
|
||||||
self._location = None
|
self._location = None
|
||||||
self._name = None
|
self._name = None
|
||||||
self._humidity = None
|
self._humidity = None
|
||||||
self._target_humidity = None
|
|
||||||
self._target_temperature = None
|
self._target_temperature = None
|
||||||
self._temperature = None
|
self._temperature = None
|
||||||
|
self._temperature_scale = None
|
||||||
self._mode = None
|
self._mode = None
|
||||||
self._fan = None
|
self._fan = None
|
||||||
self._away_temperature = None
|
self._eco_temperature = None
|
||||||
|
self._is_locked = None
|
||||||
|
self._locked_temperature = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the nest, if any."""
|
"""Return the name of the nest, if any."""
|
||||||
if self._location is None:
|
|
||||||
return self._name
|
return self._name
|
||||||
else:
|
|
||||||
if self._name == '':
|
|
||||||
return self._location.capitalize()
|
|
||||||
else:
|
|
||||||
return self._location.capitalize() + '(' + self._name + ')'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_unit(self):
|
def temperature_unit(self):
|
||||||
"""Return the unit of measurement."""
|
"""Return the unit of measurement."""
|
||||||
return TEMP_CELSIUS
|
return self._temperature_scale
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the device specific state attributes."""
|
|
||||||
if self._has_humidifier or self._has_dehumidifier:
|
|
||||||
# Move these to Thermostat Device and make them global
|
|
||||||
return {
|
|
||||||
"humidity": self._humidity,
|
|
||||||
"target_humidity": self._target_humidity,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# No way to control humidity not show setting
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
|
@ -112,21 +104,17 @@ class NestThermostat(ClimateDevice):
|
||||||
@property
|
@property
|
||||||
def current_operation(self):
|
def current_operation(self):
|
||||||
"""Return current operation ie. heat, cool, idle."""
|
"""Return current operation ie. heat, cool, idle."""
|
||||||
if self._mode == 'cool':
|
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||||
return STATE_COOL
|
return self._mode
|
||||||
elif self._mode == 'heat':
|
elif self._mode == STATE_HEAT_COOL:
|
||||||
return STATE_HEAT
|
|
||||||
elif self._mode == 'range':
|
|
||||||
return STATE_AUTO
|
return STATE_AUTO
|
||||||
elif self._mode == 'off':
|
|
||||||
return STATE_OFF
|
|
||||||
else:
|
else:
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
if self._mode != 'range' and not self.is_away_mode_on:
|
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||||
return self._target_temperature
|
return self._target_temperature
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -134,10 +122,11 @@ class NestThermostat(ClimateDevice):
|
||||||
@property
|
@property
|
||||||
def target_temperature_low(self):
|
def target_temperature_low(self):
|
||||||
"""Return the lower bound temperature we try to reach."""
|
"""Return the lower bound temperature we try to reach."""
|
||||||
if self.is_away_mode_on and self._away_temperature[0]:
|
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||||
# away_temperature is always a low, high tuple
|
self._eco_temperature[0]:
|
||||||
return self._away_temperature[0]
|
# eco_temperature is always a low, high tuple
|
||||||
if self._mode == 'range':
|
return self._eco_temperature[0]
|
||||||
|
if self._mode == STATE_HEAT_COOL:
|
||||||
return self._target_temperature[0]
|
return self._target_temperature[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -145,10 +134,11 @@ class NestThermostat(ClimateDevice):
|
||||||
@property
|
@property
|
||||||
def target_temperature_high(self):
|
def target_temperature_high(self):
|
||||||
"""Return the upper bound temperature we try to reach."""
|
"""Return the upper bound temperature we try to reach."""
|
||||||
if self.is_away_mode_on and self._away_temperature[1]:
|
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||||
# away_temperature is always a low, high tuple
|
self._eco_temperature[1]:
|
||||||
return self._away_temperature[1]
|
# eco_temperature is always a low, high tuple
|
||||||
if self._mode == 'range':
|
return self._eco_temperature[1]
|
||||||
|
if self._mode == STATE_HEAT_COOL:
|
||||||
return self._target_temperature[1]
|
return self._target_temperature[1]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -163,8 +153,7 @@ class NestThermostat(ClimateDevice):
|
||||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
if target_temp_low is not None and target_temp_high is not None:
|
if target_temp_low is not None and target_temp_high is not None:
|
||||||
|
if self._mode == STATE_HEAT_COOL:
|
||||||
if self._mode == 'range':
|
|
||||||
temp = (target_temp_low, target_temp_high)
|
temp = (target_temp_low, target_temp_high)
|
||||||
else:
|
else:
|
||||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
@ -173,14 +162,11 @@ class NestThermostat(ClimateDevice):
|
||||||
|
|
||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
"""Set operation mode."""
|
"""Set operation mode."""
|
||||||
if operation_mode == STATE_HEAT:
|
if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||||
self.device.mode = 'heat'
|
device_mode = operation_mode
|
||||||
elif operation_mode == STATE_COOL:
|
|
||||||
self.device.mode = 'cool'
|
|
||||||
elif operation_mode == STATE_AUTO:
|
elif operation_mode == STATE_AUTO:
|
||||||
self.device.mode = 'range'
|
device_mode = STATE_HEAT_COOL
|
||||||
elif operation_mode == STATE_OFF:
|
self.device.mode = device_mode
|
||||||
self.device.mode = 'off'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def operation_list(self):
|
def operation_list(self):
|
||||||
|
@ -217,30 +203,33 @@ class NestThermostat(ClimateDevice):
|
||||||
@property
|
@property
|
||||||
def min_temp(self):
|
def min_temp(self):
|
||||||
"""Identify min_temp in Nest API or defaults if not available."""
|
"""Identify min_temp in Nest API or defaults if not available."""
|
||||||
temp = self._away_temperature[0]
|
if self._is_locked:
|
||||||
if temp is None:
|
return self._locked_temperature[0]
|
||||||
return super().min_temp
|
|
||||||
else:
|
else:
|
||||||
return temp
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_temp(self):
|
def max_temp(self):
|
||||||
"""Identify max_temp in Nest API or defaults if not available."""
|
"""Identify max_temp in Nest API or defaults if not available."""
|
||||||
temp = self._away_temperature[1]
|
if self._is_locked:
|
||||||
if temp is None:
|
return self._locked_temperature[1]
|
||||||
return super().max_temp
|
|
||||||
else:
|
else:
|
||||||
return temp
|
return None
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Cache value from Python-nest."""
|
"""Cache value from Python-nest."""
|
||||||
self._location = self.device.where
|
self._location = self.device.where
|
||||||
self._name = self.device.name
|
self._name = self.device.name
|
||||||
self._humidity = self.device.humidity,
|
self._humidity = self.device.humidity,
|
||||||
self._target_humidity = self.device.target_humidity,
|
|
||||||
self._temperature = self.device.temperature
|
self._temperature = self.device.temperature
|
||||||
self._mode = self.device.mode
|
self._mode = self.device.mode
|
||||||
self._target_temperature = self.device.target
|
self._target_temperature = self.device.target
|
||||||
self._fan = self.device.fan
|
self._fan = self.device.fan
|
||||||
self._away = self.structure.away
|
self._away = self.structure.away == 'away'
|
||||||
self._away_temperature = self.device.away_temperature
|
self._eco_temperature = self.device.eco_temperature
|
||||||
|
self._locked_temperature = self.device.locked_temperature
|
||||||
|
self._is_locked = self.device.is_locked
|
||||||
|
if self.device.temperature == 'C':
|
||||||
|
self._temperature_scale = TEMP_CELSIUS
|
||||||
|
else:
|
||||||
|
self._temperature_scale = TEMP_FAHRENHEIT
|
||||||
|
|
|
@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.proliphix/
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
|
||||||
|
ClimateDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -60,6 +61,15 @@ class ProliphixThermostat(ClimateDevice):
|
||||||
"""Return the name of the thermostat."""
|
"""Return the name of the thermostat."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def precision(self):
|
||||||
|
"""Return the precision of the system.
|
||||||
|
|
||||||
|
Proliphix temperature values are passed back and forth in the
|
||||||
|
API as tenths of degrees F (i.e. 690 for 69 degrees).
|
||||||
|
"""
|
||||||
|
return PRECISION_TENTHS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the device specific state attributes."""
|
"""Return the device specific state attributes."""
|
||||||
|
|
|
@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Wink thermostat."""
|
"""Setup the Wink thermostat."""
|
||||||
import pywink
|
import pywink
|
||||||
temp_unit = hass.config.units.temperature_unit
|
temp_unit = hass.config.units.temperature_unit
|
||||||
add_devices(WinkThermostat(thermostat, temp_unit)
|
add_devices(WinkThermostat(thermostat, hass, temp_unit)
|
||||||
for thermostat in pywink.get_thermostats())
|
for thermostat in pywink.get_thermostats())
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,9 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||||
"""Representation of a Wink thermostat."""
|
"""Representation of a Wink thermostat."""
|
||||||
|
|
||||||
def __init__(self, wink, temp_unit):
|
def __init__(self, wink, hass, temp_unit):
|
||||||
"""Initialize the Wink device."""
|
"""Initialize the Wink device."""
|
||||||
super().__init__(wink)
|
super().__init__(wink, hass)
|
||||||
wink = get_component('wink')
|
wink = get_component('wink')
|
||||||
self._config_temp_unit = temp_unit
|
self._config_temp_unit = temp_unit
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@ import logging
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.components.cover import CoverDevice,\
|
from homeassistant.components.cover import CoverDevice,\
|
||||||
ATTR_POSITION
|
ATTR_POSITION
|
||||||
import homeassistant.components.homematic as homematic
|
from homeassistant.components.homematic import HMDevice
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -24,14 +25,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
homematic = get_component("homematic")
|
||||||
return homematic.setup_hmdevice_discovery_helper(
|
return homematic.setup_hmdevice_discovery_helper(
|
||||||
|
hass,
|
||||||
HMCover,
|
HMCover,
|
||||||
discovery_info,
|
discovery_info,
|
||||||
add_callback_devices
|
add_callback_devices
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HMCover(homematic.HMDevice, CoverDevice):
|
class HMCover(HMDevice, CoverDevice):
|
||||||
"""Represents a Homematic Cover in Home Assistant."""
|
"""Represents a Homematic Cover in Home Assistant."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -15,18 +15,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Wink cover platform."""
|
"""Setup the Wink cover platform."""
|
||||||
import pywink
|
import pywink
|
||||||
|
|
||||||
add_devices(WinkCoverDevice(shade) for shade in
|
add_devices(WinkCoverDevice(shade, hass) for shade in
|
||||||
pywink.get_shades())
|
pywink.get_shades())
|
||||||
add_devices(WinkCoverDevice(door) for door in
|
add_devices(WinkCoverDevice(door, hass) for door in
|
||||||
pywink.get_garage_doors())
|
pywink.get_garage_doors())
|
||||||
|
|
||||||
|
|
||||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||||
"""Representation of a Wink cover device."""
|
"""Representation of a Wink cover device."""
|
||||||
|
|
||||||
def __init__(self, wink):
|
def __init__(self, wink, hass):
|
||||||
"""Initialize the cover."""
|
"""Initialize the cover."""
|
||||||
WinkDevice.__init__(self, wink)
|
WinkDevice.__init__(self, wink, hass)
|
||||||
|
|
||||||
def close_cover(self):
|
def close_cover(self):
|
||||||
"""Close the shade."""
|
"""Close the shade."""
|
||||||
|
|
|
@ -86,16 +86,11 @@ def setup(hass, config):
|
||||||
group.Group.create_group(hass, 'people', [
|
group.Group.create_group(hass, 'people', [
|
||||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||||
'device_tracker.demo_paulus'])
|
'device_tracker.demo_paulus'])
|
||||||
group.Group.create_group(hass, 'thermostats', [
|
|
||||||
'thermostat.nest', 'thermostat.thermostat'])
|
|
||||||
group.Group.create_group(hass, 'downstairs', [
|
group.Group.create_group(hass, 'downstairs', [
|
||||||
'group.living_room', 'group.kitchen',
|
'group.living_room', 'group.kitchen',
|
||||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||||
'rollershutter.living_room_window', 'group.doors',
|
'rollershutter.living_room_window', 'group.doors',
|
||||||
'thermostat.nest',
|
'thermostat.ecobee',
|
||||||
], view=True)
|
|
||||||
group.Group.create_group(hass, 'Upstairs', [
|
|
||||||
'thermostat.thermostat', 'group.bedroom',
|
|
||||||
], view=True)
|
], view=True)
|
||||||
|
|
||||||
# Setup scripts
|
# Setup scripts
|
||||||
|
|
|
@ -10,6 +10,8 @@ import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Sequence, Callable
|
from typing import Any, Sequence, Callable
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.bootstrap import (
|
from homeassistant.bootstrap import (
|
||||||
|
@ -19,6 +21,7 @@ from homeassistant.components import group, zone
|
||||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers import config_per_platform, discovery
|
from homeassistant.helpers import config_per_platform, discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||||
|
@ -278,6 +281,9 @@ class DeviceTracker(object):
|
||||||
yield from self.group.async_update_tracked_entity_ids(
|
yield from self.group.async_update_tracked_entity_ids(
|
||||||
list(self.group.tracking) + [device.entity_id])
|
list(self.group.tracking) + [device.entity_id])
|
||||||
|
|
||||||
|
# lookup mac vendor string to be stored in config
|
||||||
|
device.set_vendor_for_mac()
|
||||||
|
|
||||||
# update known_devices.yaml
|
# update known_devices.yaml
|
||||||
self.hass.async_add_job(
|
self.hass.async_add_job(
|
||||||
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
||||||
|
@ -291,7 +297,7 @@ class DeviceTracker(object):
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
with (yield from self._is_updating):
|
with (yield from self._is_updating):
|
||||||
self.hass.loop.run_in_executor(
|
yield from self.hass.loop.run_in_executor(
|
||||||
None, update_config, self.hass.config.path(YAML_DEVICES),
|
None, update_config, self.hass.config.path(YAML_DEVICES),
|
||||||
dev_id, device)
|
dev_id, device)
|
||||||
|
|
||||||
|
@ -328,6 +334,7 @@ class Device(Entity):
|
||||||
last_seen = None # type: dt_util.dt.datetime
|
last_seen = None # type: dt_util.dt.datetime
|
||||||
battery = None # type: str
|
battery = None # type: str
|
||||||
attributes = None # type: dict
|
attributes = None # type: dict
|
||||||
|
vendor = None # type: str
|
||||||
|
|
||||||
# Track if the last update of this device was HOME.
|
# Track if the last update of this device was HOME.
|
||||||
last_update_home = False
|
last_update_home = False
|
||||||
|
@ -336,7 +343,7 @@ class Device(Entity):
|
||||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||||
track: bool, dev_id: str, mac: str, name: str=None,
|
track: bool, dev_id: str, mac: str, name: str=None,
|
||||||
picture: str=None, gravatar: str=None,
|
picture: str=None, gravatar: str=None,
|
||||||
hide_if_away: bool=False) -> None:
|
hide_if_away: bool=False, vendor: str=None) -> None:
|
||||||
"""Initialize a device."""
|
"""Initialize a device."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||||
|
@ -362,6 +369,7 @@ class Device(Entity):
|
||||||
self.config_picture = picture
|
self.config_picture = picture
|
||||||
|
|
||||||
self.away_hide = hide_if_away
|
self.away_hide = hide_if_away
|
||||||
|
self.vendor = vendor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -460,6 +468,53 @@ class Device(Entity):
|
||||||
self._state = STATE_HOME
|
self._state = STATE_HOME
|
||||||
self.last_update_home = True
|
self.last_update_home = True
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def set_vendor_for_mac(self):
|
||||||
|
"""Set vendor string using api.macvendors.com."""
|
||||||
|
self.vendor = yield from self.get_vendor_for_mac()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def get_vendor_for_mac(self):
|
||||||
|
"""Try to find the vendor string for a given MAC address."""
|
||||||
|
# can't continue without a mac
|
||||||
|
if not self.mac:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# prevent lookup of invalid macs
|
||||||
|
if not len(self.mac.split(':')) == 6:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
# we only need the first 3 bytes of the mac for a lookup
|
||||||
|
# this improves somewhat on privacy
|
||||||
|
oui_bytes = self.mac.split(':')[0:3]
|
||||||
|
# bytes like 00 get truncates to 0, API needs full bytes
|
||||||
|
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||||
|
url = 'http://api.macvendors.com/' + oui
|
||||||
|
resp = None
|
||||||
|
try:
|
||||||
|
websession = async_get_clientsession(self.hass)
|
||||||
|
|
||||||
|
with async_timeout.timeout(5, loop=self.hass.loop):
|
||||||
|
resp = yield from websession.get(url)
|
||||||
|
# mac vendor found, response is the string
|
||||||
|
if resp.status == 200:
|
||||||
|
vendor_string = yield from resp.text()
|
||||||
|
return vendor_string
|
||||||
|
# if vendor is not known to the API (404) or there
|
||||||
|
# was a failure during the lookup (500); set vendor
|
||||||
|
# to something other then None to prevent retry
|
||||||
|
# as the value is only relevant when it is to be stored
|
||||||
|
# in the 'known_devices.yaml' file which only happens
|
||||||
|
# the first time the device is seen.
|
||||||
|
return 'unknown'
|
||||||
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
|
||||||
|
aiohttp.errors.ClientDisconnectedError):
|
||||||
|
# same as above
|
||||||
|
return 'unknown'
|
||||||
|
finally:
|
||||||
|
if resp is not None:
|
||||||
|
yield from resp.release()
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||||
"""Load devices from YAML configuration file."""
|
"""Load devices from YAML configuration file."""
|
||||||
|
@ -483,7 +538,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||||
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
|
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
|
||||||
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||||
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta),
|
||||||
|
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
result = []
|
result = []
|
||||||
|
@ -546,7 +602,8 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||||
'mac': device.mac,
|
'mac': device.mac,
|
||||||
'picture': device.config_picture,
|
'picture': device.config_picture,
|
||||||
'track': device.track,
|
'track': device.track,
|
||||||
CONF_AWAY_HIDE: device.away_hide
|
CONF_AWAY_HIDE: device.away_hide,
|
||||||
|
'vendor': device.vendor,
|
||||||
}}
|
}}
|
||||||
out.write('\n')
|
out.write('\n')
|
||||||
out.write(dump(device))
|
out.write(dump(device))
|
||||||
|
|
72
homeassistant/components/device_tracker/gpslogger.py
Normal file
72
homeassistant/components/device_tracker/gpslogger.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
"""
|
||||||
|
Support for the GPSLogger platform.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.gpslogger/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
# pylint: disable=unused-import
|
||||||
|
from homeassistant.components.device_tracker import ( # NOQA
|
||||||
|
DOMAIN, PLATFORM_SCHEMA)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config, see):
|
||||||
|
"""Setup an endpoint for the GPSLogger application."""
|
||||||
|
hass.http.register_view(GPSLoggerView(see))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class GPSLoggerView(HomeAssistantView):
|
||||||
|
"""View to handle gpslogger requests."""
|
||||||
|
|
||||||
|
url = '/api/gpslogger'
|
||||||
|
name = 'api:gpslogger'
|
||||||
|
|
||||||
|
def __init__(self, see):
|
||||||
|
"""Initialize GPSLogger url endpoints."""
|
||||||
|
self.see = see
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def get(self, request):
|
||||||
|
"""A GPSLogger message received as GET."""
|
||||||
|
res = yield from self._handle(request.app['hass'], request.GET)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _handle(self, hass, data):
|
||||||
|
"""Handle gpslogger request."""
|
||||||
|
if 'latitude' not in data or 'longitude' not in data:
|
||||||
|
return ('Latitude and longitude not specified.',
|
||||||
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
|
if 'device' not in data:
|
||||||
|
_LOGGER.error('Device id not specified.')
|
||||||
|
return ('Device id not specified.',
|
||||||
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
|
device = data['device'].replace('-', '')
|
||||||
|
gps_location = (data['latitude'], data['longitude'])
|
||||||
|
accuracy = 200
|
||||||
|
battery = -1
|
||||||
|
|
||||||
|
if 'accuracy' in data:
|
||||||
|
accuracy = int(float(data['accuracy']))
|
||||||
|
if 'battery' in data:
|
||||||
|
battery = float(data['battery'])
|
||||||
|
|
||||||
|
yield from hass.loop.run_in_executor(
|
||||||
|
None, partial(self.see, dev_id=device,
|
||||||
|
gps=gps_location, battery=battery,
|
||||||
|
gps_accuracy=accuracy))
|
||||||
|
|
||||||
|
return 'Setting location for {}'.format(device)
|
|
@ -23,7 +23,7 @@ DEPENDENCIES = ['http']
|
||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config, see):
|
||||||
"""Setup an endpoint for the Locative application."""
|
"""Setup an endpoint for the Locative application."""
|
||||||
hass.http.register_view(LocativeView(hass, see))
|
hass.http.register_view(LocativeView(see))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -34,27 +34,26 @@ class LocativeView(HomeAssistantView):
|
||||||
url = '/api/locative'
|
url = '/api/locative'
|
||||||
name = 'api:locative'
|
name = 'api:locative'
|
||||||
|
|
||||||
def __init__(self, hass, see):
|
def __init__(self, see):
|
||||||
"""Initialize Locative url endpoints."""
|
"""Initialize Locative url endpoints."""
|
||||||
super().__init__(hass)
|
|
||||||
self.see = see
|
self.see = see
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Locative message received as GET."""
|
"""Locative message received as GET."""
|
||||||
res = yield from self._handle(request.GET)
|
res = yield from self._handle(request.app['hass'], request.GET)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Locative message received."""
|
"""Locative message received."""
|
||||||
data = yield from request.post()
|
data = yield from request.post()
|
||||||
res = yield from self._handle(data)
|
res = yield from self._handle(request.app['hass'], data)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
def _handle(self, data):
|
def _handle(self, hass, data):
|
||||||
"""Handle locative request."""
|
"""Handle locative request."""
|
||||||
if 'latitude' not in data or 'longitude' not in data:
|
if 'latitude' not in data or 'longitude' not in data:
|
||||||
return ('Latitude and longitude not specified.',
|
return ('Latitude and longitude not specified.',
|
||||||
|
@ -81,19 +80,19 @@ class LocativeView(HomeAssistantView):
|
||||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||||
|
|
||||||
if direction == 'enter':
|
if direction == 'enter':
|
||||||
yield from self.hass.loop.run_in_executor(
|
yield from hass.loop.run_in_executor(
|
||||||
None, partial(self.see, dev_id=device,
|
None, partial(self.see, dev_id=device,
|
||||||
location_name=location_name,
|
location_name=location_name,
|
||||||
gps=gps_location))
|
gps=gps_location))
|
||||||
return 'Setting location to {}'.format(location_name)
|
return 'Setting location to {}'.format(location_name)
|
||||||
|
|
||||||
elif direction == 'exit':
|
elif direction == 'exit':
|
||||||
current_state = self.hass.states.get(
|
current_state = hass.states.get(
|
||||||
'{}.{}'.format(DOMAIN, device))
|
'{}.{}'.format(DOMAIN, device))
|
||||||
|
|
||||||
if current_state is None or current_state.state == location_name:
|
if current_state is None or current_state.state == location_name:
|
||||||
location_name = STATE_NOT_HOME
|
location_name = STATE_NOT_HOME
|
||||||
yield from self.hass.loop.run_in_executor(
|
yield from hass.loop.run_in_executor(
|
||||||
None, partial(self.see, dev_id=device,
|
None, partial(self.see, dev_id=device,
|
||||||
location_name=location_name,
|
location_name=location_name,
|
||||||
gps=gps_location))
|
gps=gps_location))
|
||||||
|
|
|
@ -75,14 +75,16 @@ def setup(hass, yaml_config):
|
||||||
api_password=None,
|
api_password=None,
|
||||||
ssl_certificate=None,
|
ssl_certificate=None,
|
||||||
ssl_key=None,
|
ssl_key=None,
|
||||||
cors_origins=[],
|
cors_origins=None,
|
||||||
use_x_forwarded_for=False,
|
use_x_forwarded_for=False,
|
||||||
trusted_networks=[]
|
trusted_networks=[],
|
||||||
|
login_threshold=0,
|
||||||
|
is_ban_enabled=False
|
||||||
)
|
)
|
||||||
|
|
||||||
server.register_view(DescriptionXmlView(hass, config))
|
server.register_view(DescriptionXmlView(config))
|
||||||
server.register_view(HueUsernameView(hass))
|
server.register_view(HueUsernameView)
|
||||||
server.register_view(HueLightsView(hass, config))
|
server.register_view(HueLightsView(config))
|
||||||
|
|
||||||
upnp_listener = UPNPResponderThread(
|
upnp_listener = UPNPResponderThread(
|
||||||
config.host_ip_addr, config.listen_port)
|
config.host_ip_addr, config.listen_port)
|
||||||
|
@ -154,9 +156,8 @@ class DescriptionXmlView(HomeAssistantView):
|
||||||
name = 'description:xml'
|
name = 'description:xml'
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, config):
|
||||||
"""Initialize the instance of the view."""
|
"""Initialize the instance of the view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
|
@ -198,10 +199,6 @@ class HueUsernameView(HomeAssistantView):
|
||||||
extra_urls = ['/api/']
|
extra_urls = ['/api/']
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, hass):
|
|
||||||
"""Initialize the instance of the view."""
|
|
||||||
super().__init__(hass)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Handle a POST request."""
|
"""Handle a POST request."""
|
||||||
|
@ -226,30 +223,33 @@ class HueLightsView(HomeAssistantView):
|
||||||
'/api/{username}/lights/{entity_id}/state']
|
'/api/{username}/lights/{entity_id}/state']
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, config):
|
||||||
"""Initialize the instance of the view."""
|
"""Initialize the instance of the view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.cached_states = {}
|
self.cached_states = {}
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
def get(self, request, username, entity_id=None):
|
def get(self, request, username, entity_id=None):
|
||||||
"""Handle a GET request."""
|
"""Handle a GET request."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
|
||||||
if entity_id is None:
|
if entity_id is None:
|
||||||
return self.async_get_lights_list()
|
return self.async_get_lights_list(hass)
|
||||||
|
|
||||||
if not request.path.endswith('state'):
|
if not request.path.endswith('state'):
|
||||||
return self.async_get_light_state(entity_id)
|
return self.async_get_light_state(hass, entity_id)
|
||||||
|
|
||||||
return web.Response(text="Method not allowed", status=405)
|
return web.Response(text="Method not allowed", status=405)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def put(self, request, username, entity_id=None):
|
def put(self, request, username, entity_id=None):
|
||||||
"""Handle a PUT request."""
|
"""Handle a PUT request."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
|
||||||
if not request.path.endswith('state'):
|
if not request.path.endswith('state'):
|
||||||
return web.Response(text="Method not allowed", status=405)
|
return web.Response(text="Method not allowed", status=405)
|
||||||
|
|
||||||
if entity_id and self.hass.states.get(entity_id) is None:
|
if entity_id and hass.states.get(entity_id) is None:
|
||||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -257,24 +257,25 @@ class HueLightsView(HomeAssistantView):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
result = yield from self.async_put_light_state(json_data, entity_id)
|
result = yield from self.async_put_light_state(hass, json_data,
|
||||||
|
entity_id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
def async_get_lights_list(self):
|
def async_get_lights_list(self, hass):
|
||||||
"""Process a request to get the list of available lights."""
|
"""Process a request to get the list of available lights."""
|
||||||
json_response = {}
|
json_response = {}
|
||||||
|
|
||||||
for entity in self.hass.states.async_all():
|
for entity in hass.states.async_all():
|
||||||
if self.is_entity_exposed(entity):
|
if self.is_entity_exposed(entity):
|
||||||
json_response[entity.entity_id] = entity_to_json(entity)
|
json_response[entity.entity_id] = entity_to_json(entity)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
def async_get_light_state(self, entity_id):
|
def async_get_light_state(self, hass, entity_id):
|
||||||
"""Process a request to get the state of an individual light."""
|
"""Process a request to get the state of an individual light."""
|
||||||
entity = self.hass.states.get(entity_id)
|
entity = hass.states.get(entity_id)
|
||||||
if entity is None or not self.is_entity_exposed(entity):
|
if entity is None or not self.is_entity_exposed(entity):
|
||||||
return web.Response(text="Entity not found", status=404)
|
return web.Response(text="Entity not found", status=404)
|
||||||
|
|
||||||
|
@ -292,12 +293,12 @@ class HueLightsView(HomeAssistantView):
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_put_light_state(self, request_json, entity_id):
|
def async_put_light_state(self, hass, request_json, entity_id):
|
||||||
"""Process a request to set the state of an individual light."""
|
"""Process a request to set the state of an individual light."""
|
||||||
config = self.config
|
config = self.config
|
||||||
|
|
||||||
# Retrieve the entity from the state machine
|
# Retrieve the entity from the state machine
|
||||||
entity = self.hass.states.get(entity_id)
|
entity = hass.states.get(entity_id)
|
||||||
if entity is None:
|
if entity is None:
|
||||||
return web.Response(text="Entity not found", status=404)
|
return web.Response(text="Entity not found", status=404)
|
||||||
|
|
||||||
|
@ -342,7 +343,7 @@ class HueLightsView(HomeAssistantView):
|
||||||
self.cached_states[entity_id] = (result, brightness)
|
self.cached_states[entity_id] = (result, brightness)
|
||||||
|
|
||||||
# Perform the requested action
|
# Perform the requested action
|
||||||
yield from self.hass.services.async_call(core.DOMAIN, service, data,
|
yield from hass.services.async_call(core.DOMAIN, service, data,
|
||||||
blocking=True)
|
blocking=True)
|
||||||
|
|
||||||
json_response = \
|
json_response = \
|
||||||
|
|
|
@ -75,8 +75,7 @@ def setup(hass, config):
|
||||||
descriptions[DOMAIN][SERVICE_CHECKIN],
|
descriptions[DOMAIN][SERVICE_CHECKIN],
|
||||||
schema=CHECKIN_SERVICE_SCHEMA)
|
schema=CHECKIN_SERVICE_SCHEMA)
|
||||||
|
|
||||||
hass.http.register_view(FoursquarePushReceiver(
|
hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET]))
|
||||||
hass, config[CONF_PUSH_SECRET]))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -88,9 +87,8 @@ class FoursquarePushReceiver(HomeAssistantView):
|
||||||
url = "/api/foursquare"
|
url = "/api/foursquare"
|
||||||
name = "foursquare"
|
name = "foursquare"
|
||||||
|
|
||||||
def __init__(self, hass, push_secret):
|
def __init__(self, push_secret):
|
||||||
"""Initialize the OAuth callback view."""
|
"""Initialize the OAuth callback view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.push_secret = push_secret
|
self.push_secret = push_secret
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -110,4 +108,4 @@ class FoursquarePushReceiver(HomeAssistantView):
|
||||||
"push secret: %s", secret)
|
"push secret: %s", secret)
|
||||||
return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
|
return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_PUSH, data)
|
request.app['hass'].bus.async_fire(EVENT_PUSH, data)
|
||||||
|
|
|
@ -8,17 +8,18 @@ import os
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND
|
from homeassistant.const import HTTP_NOT_FOUND
|
||||||
from homeassistant.components import api, group
|
from homeassistant.components import api, group
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
from homeassistant.components.http.auth import is_trusted_ip
|
||||||
|
from homeassistant.components.http.const import KEY_DEVELOPMENT
|
||||||
from .version import FINGERPRINTS
|
from .version import FINGERPRINTS
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api']
|
DEPENDENCIES = ['api', 'websocket_api']
|
||||||
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
||||||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||||
PANELS = {}
|
|
||||||
MANIFEST_JSON = {
|
MANIFEST_JSON = {
|
||||||
"background_color": "#FFFFFF",
|
"background_color": "#FFFFFF",
|
||||||
"description": "Open-source home automation platform running on Python 3.",
|
"description": "Open-source home automation platform running on Python 3.",
|
||||||
|
@ -32,6 +33,16 @@ MANIFEST_JSON = {
|
||||||
"theme_color": "#03A9F4"
|
"theme_color": "#03A9F4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for size in (192, 384, 512, 1024):
|
||||||
|
MANIFEST_JSON['icons'].append({
|
||||||
|
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
|
||||||
|
"sizes": "{}x{}".format(size, size),
|
||||||
|
"type": "image/png"
|
||||||
|
})
|
||||||
|
|
||||||
|
DATA_PANELS = 'frontend_panels'
|
||||||
|
DATA_INDEX_VIEW = 'frontend_index_view'
|
||||||
|
|
||||||
# To keep track we don't register a component twice (gives a warning)
|
# To keep track we don't register a component twice (gives a warning)
|
||||||
_REGISTERED_COMPONENTS = set()
|
_REGISTERED_COMPONENTS = set()
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -68,10 +79,14 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||||
|
|
||||||
Warning: this API will probably change. Use at own risk.
|
Warning: this API will probably change. Use at own risk.
|
||||||
"""
|
"""
|
||||||
|
panels = hass.data.get(DATA_PANELS)
|
||||||
|
if panels is None:
|
||||||
|
panels = hass.data[DATA_PANELS] = {}
|
||||||
|
|
||||||
if url_path is None:
|
if url_path is None:
|
||||||
url_path = component_name
|
url_path = component_name
|
||||||
|
|
||||||
if url_path in PANELS:
|
if url_path in panels:
|
||||||
_LOGGER.warning('Overwriting component %s', url_path)
|
_LOGGER.warning('Overwriting component %s', url_path)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
_LOGGER.error('Panel %s component does not exist: %s',
|
_LOGGER.error('Panel %s component does not exist: %s',
|
||||||
|
@ -106,7 +121,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||||
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
||||||
data['url'] = fprinted_url
|
data['url'] = fprinted_url
|
||||||
|
|
||||||
PANELS[url_path] = data
|
panels[url_path] = data
|
||||||
|
|
||||||
|
# Register index view for this route if IndexView already loaded
|
||||||
|
# Otherwise it will be done during setup.
|
||||||
|
index_view = hass.data.get(DATA_INDEX_VIEW)
|
||||||
|
|
||||||
|
if index_view:
|
||||||
|
hass.http.app.router.add_route('get', '/{}'.format(url_path),
|
||||||
|
index_view.get)
|
||||||
|
|
||||||
|
|
||||||
def add_manifest_json_key(key, val):
|
def add_manifest_json_key(key, val):
|
||||||
|
@ -134,29 +157,24 @@ def setup(hass, config):
|
||||||
if os.path.isdir(local):
|
if os.path.isdir(local):
|
||||||
hass.http.register_static_path("/local", local)
|
hass.http.register_static_path("/local", local)
|
||||||
|
|
||||||
|
index_view = hass.data[DATA_INDEX_VIEW] = IndexView()
|
||||||
|
hass.http.register_view(index_view)
|
||||||
|
|
||||||
|
# Components have registered panels before frontend got setup.
|
||||||
|
# Now register their urls.
|
||||||
|
if DATA_PANELS in hass.data:
|
||||||
|
for url_path in hass.data[DATA_PANELS]:
|
||||||
|
hass.http.app.router.add_route('get', '/{}'.format(url_path),
|
||||||
|
index_view.get)
|
||||||
|
else:
|
||||||
|
hass.data[DATA_PANELS] = {}
|
||||||
|
|
||||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||||
|
|
||||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||||
'dev-template'):
|
'dev-template'):
|
||||||
register_built_in_panel(hass, panel)
|
register_built_in_panel(hass, panel)
|
||||||
|
|
||||||
def register_frontend_index(event):
|
|
||||||
"""Register the frontend index urls.
|
|
||||||
|
|
||||||
Done when Home Assistant is started so that all panels are known.
|
|
||||||
"""
|
|
||||||
hass.http.register_view(IndexView(
|
|
||||||
hass, ['/{}'.format(name) for name in PANELS]))
|
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
|
||||||
|
|
||||||
for size in (192, 384, 512, 1024):
|
|
||||||
MANIFEST_JSON['icons'].append({
|
|
||||||
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
|
|
||||||
"sizes": "{}x{}".format(size, size),
|
|
||||||
"type": "image/png"
|
|
||||||
})
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -169,12 +187,14 @@ class BootstrapView(HomeAssistantView):
|
||||||
@callback
|
@callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Return all data needed to bootstrap Home Assistant."""
|
"""Return all data needed to bootstrap Home Assistant."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
|
||||||
return self.json({
|
return self.json({
|
||||||
'config': self.hass.config.as_dict(),
|
'config': hass.config.as_dict(),
|
||||||
'states': self.hass.states.async_all(),
|
'states': hass.states.async_all(),
|
||||||
'events': api.async_events_json(self.hass),
|
'events': api.async_events_json(hass),
|
||||||
'services': api.async_services_json(self.hass),
|
'services': api.async_services_json(hass),
|
||||||
'panels': PANELS,
|
'panels': hass.data[DATA_PANELS],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,13 +206,10 @@ class IndexView(HomeAssistantView):
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
extra_urls = ['/states', '/states/{entity_id}']
|
extra_urls = ['/states', '/states/{entity_id}']
|
||||||
|
|
||||||
def __init__(self, hass, extra_urls):
|
def __init__(self):
|
||||||
"""Initialize the frontend view."""
|
"""Initialize the frontend view."""
|
||||||
super().__init__(hass)
|
|
||||||
|
|
||||||
from jinja2 import FileSystemLoader, Environment
|
from jinja2 import FileSystemLoader, Environment
|
||||||
|
|
||||||
self.extra_urls = self.extra_urls + extra_urls
|
|
||||||
self.templates = Environment(
|
self.templates = Environment(
|
||||||
loader=FileSystemLoader(
|
loader=FileSystemLoader(
|
||||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||||
|
@ -202,14 +219,16 @@ class IndexView(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request, entity_id=None):
|
def get(self, request, entity_id=None):
|
||||||
"""Serve the index view."""
|
"""Serve the index view."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
|
||||||
if entity_id is not None:
|
if entity_id is not None:
|
||||||
state = self.hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if (not state or state.domain != 'group' or
|
if (not state or state.domain != 'group' or
|
||||||
not state.attributes.get(group.ATTR_VIEW)):
|
not state.attributes.get(group.ATTR_VIEW)):
|
||||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
|
|
||||||
if self.hass.http.development:
|
if request.app[KEY_DEVELOPMENT]:
|
||||||
core_url = '/static/home-assistant-polymer/build/core.js'
|
core_url = '/static/home-assistant-polymer/build/core.js'
|
||||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||||
else:
|
else:
|
||||||
|
@ -223,19 +242,21 @@ class IndexView(HomeAssistantView):
|
||||||
else:
|
else:
|
||||||
panel = request.path.split('/')[1]
|
panel = request.path.split('/')[1]
|
||||||
|
|
||||||
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
|
if panel == 'states':
|
||||||
|
panel_url = ''
|
||||||
|
else:
|
||||||
|
panel_url = hass.data[DATA_PANELS][panel]['url']
|
||||||
|
|
||||||
no_auth = 'true'
|
no_auth = 'true'
|
||||||
if self.hass.config.api.api_password:
|
if hass.config.api.api_password:
|
||||||
# require password if set
|
# require password if set
|
||||||
no_auth = 'false'
|
no_auth = 'false'
|
||||||
if self.hass.http.is_trusted_ip(
|
if is_trusted_ip(request):
|
||||||
self.hass.http.get_real_ip(request)):
|
|
||||||
# bypass for trusted networks
|
# bypass for trusted networks
|
||||||
no_auth = 'true'
|
no_auth = 'true'
|
||||||
|
|
||||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||||
template = yield from self.hass.loop.run_in_executor(
|
template = yield from hass.loop.run_in_executor(
|
||||||
None, self.templates.get_template, 'index.html')
|
None, self.templates.get_template, 'index.html')
|
||||||
|
|
||||||
# pylint is wrong
|
# pylint is wrong
|
||||||
|
@ -244,7 +265,7 @@ class IndexView(HomeAssistantView):
|
||||||
resp = template.render(
|
resp = template.render(
|
||||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||||
panel_url=panel_url, panels=PANELS)
|
panel_url=panel_url, panels=hass.data[DATA_PANELS])
|
||||||
|
|
||||||
return web.Response(text=resp, content_type='text/html')
|
return web.Response(text=resp, content_type='text/html')
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||||
|
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
|
"core.js": "526d7d704ae478c30ae20c1426c2e4f4",
|
||||||
"frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d",
|
"frontend.html": "5baa4dc3b109ca80d4c282fb12c6c23a",
|
||||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
||||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||||
"panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769",
|
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
|
||||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
||||||
"panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e"
|
"panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
|
||||||
|
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
||||||
Subproject commit 6071315b1675dfef1090b4683c9639ef0f56cfc0
|
Subproject commit b76ad67d4abbc0cc492fc11842c9d163b4917ead
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
125
homeassistant/components/frontend/www_static/websocket_test.html
Normal file
125
homeassistant/components/frontend/www_static/websocket_test.html
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>WebSocket debug</title>
|
||||||
|
<style>
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls textarea {
|
||||||
|
height: 160px;
|
||||||
|
min-width: 400px;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='controls'>
|
||||||
|
<textarea id="messageinput">
|
||||||
|
{
|
||||||
|
"id": 1, "type": "subscribe_events", "event_type": "state_changed"
|
||||||
|
}
|
||||||
|
</textarea>
|
||||||
|
<pre>
|
||||||
|
Examples:
|
||||||
|
{
|
||||||
|
"id": 2, "type": "subscribe_events", "event_type": "state_changed"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 4, "type": "unsubscribe_events", "subscription": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 5, "type": "get_states"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 6, "type": "get_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 7, "type": "get_services"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 8, "type": "get_panels"
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" onclick="openSocket();" >Open</button>
|
||||||
|
<button type="button" onclick="send();" >Send</button>
|
||||||
|
<button type="button" onclick="closeSocket();" >Close</button>
|
||||||
|
</div>
|
||||||
|
<!-- Server responses get written here -->
|
||||||
|
<pre id="messages"></pre>
|
||||||
|
|
||||||
|
<!-- Script to utilise the WebSocket -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var webSocket;
|
||||||
|
var messages = document.getElementById("messages");
|
||||||
|
|
||||||
|
function openSocket(){
|
||||||
|
var isOpen = false;
|
||||||
|
// Ensures only one connection is open at a time
|
||||||
|
if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
|
||||||
|
writeResponse("WebSocket is already opened.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create a new instance of the websocket
|
||||||
|
webSocket = new WebSocket("ws://localhost:8123/api/websocket");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds functions to the listeners for the websocket.
|
||||||
|
*/
|
||||||
|
webSocket.onopen = function(event){
|
||||||
|
if (!isOpen) {
|
||||||
|
isOpen = true;
|
||||||
|
writeResponse('Connection opened');
|
||||||
|
}
|
||||||
|
// For reasons I can't determine, onopen gets called twice
|
||||||
|
// and the first time event.data is undefined.
|
||||||
|
// Leave a comment if you know the answer.
|
||||||
|
if(event.data === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
writeResponse(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
webSocket.onmessage = function(event){
|
||||||
|
writeResponse(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
webSocket.onclose = function(event){
|
||||||
|
writeResponse("Connection closed");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the value of the text input to the server
|
||||||
|
*/
|
||||||
|
function send(){
|
||||||
|
var text = document.getElementById("messageinput").value;
|
||||||
|
webSocket.send(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSocket(){
|
||||||
|
webSocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeResponse(text){
|
||||||
|
messages.innerHTML += "\n" + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
openSocket();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
|
@ -184,8 +184,8 @@ def setup(hass, config):
|
||||||
filters.included_entities = include[CONF_ENTITIES]
|
filters.included_entities = include[CONF_ENTITIES]
|
||||||
filters.included_domains = include[CONF_DOMAINS]
|
filters.included_domains = include[CONF_DOMAINS]
|
||||||
|
|
||||||
hass.http.register_view(Last5StatesView(hass))
|
hass.http.register_view(Last5StatesView)
|
||||||
hass.http.register_view(HistoryPeriodView(hass, filters))
|
hass.http.register_view(HistoryPeriodView(filters))
|
||||||
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -197,14 +197,10 @@ class Last5StatesView(HomeAssistantView):
|
||||||
url = '/api/history/entity/{entity_id}/recent_states'
|
url = '/api/history/entity/{entity_id}/recent_states'
|
||||||
name = 'api:history:entity-recent-states'
|
name = 'api:history:entity-recent-states'
|
||||||
|
|
||||||
def __init__(self, hass):
|
|
||||||
"""Initilalize the history last 5 states view."""
|
|
||||||
super().__init__(hass)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request, entity_id):
|
def get(self, request, entity_id):
|
||||||
"""Retrieve last 5 states of entity."""
|
"""Retrieve last 5 states of entity."""
|
||||||
result = yield from self.hass.loop.run_in_executor(
|
result = yield from request.app['hass'].loop.run_in_executor(
|
||||||
None, last_5_states, entity_id)
|
None, last_5_states, entity_id)
|
||||||
return self.json(result)
|
return self.json(result)
|
||||||
|
|
||||||
|
@ -216,9 +212,8 @@ class HistoryPeriodView(HomeAssistantView):
|
||||||
name = 'api:history:view-period'
|
name = 'api:history:view-period'
|
||||||
extra_urls = ['/api/history/period/{datetime}']
|
extra_urls = ['/api/history/period/{datetime}']
|
||||||
|
|
||||||
def __init__(self, hass, filters):
|
def __init__(self, filters):
|
||||||
"""Initilalize the history period view."""
|
"""Initilalize the history period view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.filters = filters
|
self.filters = filters
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -240,7 +235,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||||
end_time = start_time + one_day
|
end_time = start_time + one_day
|
||||||
entity_id = request.GET.get('filter_entity_id')
|
entity_id = request.GET.get('filter_entity_id')
|
||||||
|
|
||||||
result = yield from self.hass.loop.run_in_executor(
|
result = yield from request.app['hass'].loop.run_in_executor(
|
||||||
None, get_significant_states, start_time, end_time, entity_id,
|
None, get_significant_states, start_time, end_time, entity_id,
|
||||||
self.filters)
|
self.filters)
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ from functools import partial
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
|
from homeassistant.const import (
|
||||||
CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
|
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD,
|
||||||
ATTR_ENTITY_ID)
|
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
|
@ -23,13 +23,10 @@ from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
REQUIREMENTS = ["pyhomematic==0.1.16"]
|
REQUIREMENTS = ["pyhomematic==0.1.18"]
|
||||||
|
|
||||||
HOMEMATIC = None
|
|
||||||
HOMEMATIC_LINK_DELAY = 0.5
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
|
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
|
||||||
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=60)
|
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=30)
|
||||||
|
|
||||||
DISCOVER_SWITCHES = 'homematic.switch'
|
DISCOVER_SWITCHES = 'homematic.switch'
|
||||||
DISCOVER_LIGHTS = 'homematic.light'
|
DISCOVER_LIGHTS = 'homematic.light'
|
||||||
|
@ -44,12 +41,15 @@ ATTR_CHANNEL = 'channel'
|
||||||
ATTR_NAME = 'name'
|
ATTR_NAME = 'name'
|
||||||
ATTR_ADDRESS = 'address'
|
ATTR_ADDRESS = 'address'
|
||||||
ATTR_VALUE = 'value'
|
ATTR_VALUE = 'value'
|
||||||
|
ATTR_PROXY = 'proxy'
|
||||||
|
|
||||||
EVENT_KEYPRESS = 'homematic.keypress'
|
EVENT_KEYPRESS = 'homematic.keypress'
|
||||||
EVENT_IMPULSE = 'homematic.impulse'
|
EVENT_IMPULSE = 'homematic.impulse'
|
||||||
|
|
||||||
SERVICE_VIRTUALKEY = 'virtualkey'
|
SERVICE_VIRTUALKEY = 'virtualkey'
|
||||||
SERVICE_SET_VALUE = 'set_value'
|
SERVICE_RECONNECT = 'reconnect'
|
||||||
|
SERVICE_SET_VAR_VALUE = 'set_var_value'
|
||||||
|
SERVICE_SET_DEV_VALUE = 'set_dev_value'
|
||||||
|
|
||||||
HM_DEVICE_TYPES = {
|
HM_DEVICE_TYPES = {
|
||||||
DISCOVER_SWITCHES: [
|
DISCOVER_SWITCHES: [
|
||||||
|
@ -109,44 +109,60 @@ CONF_RESOLVENAMES_OPTIONS = [
|
||||||
False
|
False
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DATA_HOMEMATIC = 'homematic'
|
||||||
|
DATA_DELAY = 'homematic_delay'
|
||||||
|
DATA_DEVINIT = 'homematic_devinit'
|
||||||
|
DATA_STORE = 'homematic_store'
|
||||||
|
|
||||||
CONF_LOCAL_IP = 'local_ip'
|
CONF_LOCAL_IP = 'local_ip'
|
||||||
CONF_LOCAL_PORT = 'local_port'
|
CONF_LOCAL_PORT = 'local_port'
|
||||||
CONF_REMOTE_IP = 'remote_ip'
|
CONF_IP = 'ip'
|
||||||
CONF_REMOTE_PORT = 'remote_port'
|
CONF_PORT = 'port'
|
||||||
CONF_RESOLVENAMES = 'resolvenames'
|
CONF_RESOLVENAMES = 'resolvenames'
|
||||||
CONF_DELAY = 'delay'
|
|
||||||
CONF_VARIABLES = 'variables'
|
CONF_VARIABLES = 'variables'
|
||||||
|
CONF_DEVICES = 'devices'
|
||||||
|
CONF_DELAY = 'delay'
|
||||||
|
CONF_PRIMARY = 'primary'
|
||||||
|
|
||||||
DEFAULT_LOCAL_IP = "0.0.0.0"
|
DEFAULT_LOCAL_IP = "0.0.0.0"
|
||||||
DEFAULT_LOCAL_PORT = 0
|
DEFAULT_LOCAL_PORT = 0
|
||||||
DEFAULT_RESOLVENAMES = False
|
DEFAULT_RESOLVENAMES = False
|
||||||
DEFAULT_REMOTE_PORT = 2001
|
DEFAULT_PORT = 2001
|
||||||
DEFAULT_USERNAME = "Admin"
|
DEFAULT_USERNAME = "Admin"
|
||||||
DEFAULT_PASSWORD = ""
|
DEFAULT_PASSWORD = ""
|
||||||
DEFAULT_VARIABLES = False
|
DEFAULT_VARIABLES = False
|
||||||
|
DEFAULT_DEVICES = True
|
||||||
DEFAULT_DELAY = 0.5
|
DEFAULT_DELAY = 0.5
|
||||||
|
DEFAULT_PRIMARY = False
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): "homematic",
|
vol.Required(CONF_PLATFORM): "homematic",
|
||||||
vol.Required(ATTR_NAME): cv.string,
|
vol.Required(ATTR_NAME): cv.string,
|
||||||
vol.Required(ATTR_ADDRESS): cv.string,
|
vol.Required(ATTR_ADDRESS): cv.string,
|
||||||
|
vol.Required(ATTR_PROXY): cv.string,
|
||||||
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
|
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
|
||||||
vol.Optional(ATTR_PARAM): cv.string,
|
vol.Optional(ATTR_PARAM): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Required(CONF_REMOTE_IP): cv.string,
|
vol.Required(CONF_HOSTS): {cv.match_all: {
|
||||||
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
|
vol.Required(CONF_IP): cv.string,
|
||||||
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
||||||
vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port,
|
cv.port,
|
||||||
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
|
|
||||||
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
|
||||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
|
||||||
|
cv.boolean,
|
||||||
|
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
|
||||||
|
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
||||||
|
vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean,
|
||||||
|
vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean,
|
||||||
|
}},
|
||||||
|
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
|
||||||
|
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
|
||||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
||||||
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
|
|
||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
@ -154,106 +170,156 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
|
||||||
vol.Required(ATTR_ADDRESS): cv.string,
|
vol.Required(ATTR_ADDRESS): cv.string,
|
||||||
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
||||||
vol.Required(ATTR_PARAM): cv.string,
|
vol.Required(ATTR_PARAM): cv.string,
|
||||||
|
vol.Optional(ATTR_PROXY): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
SCHEMA_SERVICE_SET_VALUE = vol.Schema({
|
SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Required(ATTR_VALUE): cv.match_all,
|
vol.Required(ATTR_VALUE): cv.match_all,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({
|
||||||
|
vol.Required(ATTR_ADDRESS): cv.string,
|
||||||
|
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
||||||
|
vol.Required(ATTR_PARAM): cv.string,
|
||||||
|
vol.Required(ATTR_VALUE): cv.match_all,
|
||||||
|
vol.Optional(ATTR_PROXY): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
def virtualkey(hass, address, channel, param):
|
SCHEMA_SERVICE_RECONNECT = vol.Schema({})
|
||||||
|
|
||||||
|
|
||||||
|
def virtualkey(hass, address, channel, param, proxy=None):
|
||||||
"""Send virtual keypress to homematic controlller."""
|
"""Send virtual keypress to homematic controlller."""
|
||||||
data = {
|
data = {
|
||||||
ATTR_ADDRESS: address,
|
ATTR_ADDRESS: address,
|
||||||
ATTR_CHANNEL: channel,
|
ATTR_CHANNEL: channel,
|
||||||
ATTR_PARAM: param,
|
ATTR_PARAM: param,
|
||||||
|
ATTR_PROXY: proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
|
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
|
||||||
|
|
||||||
|
|
||||||
def set_value(hass, entity_id, value):
|
def set_var_value(hass, entity_id, value):
|
||||||
"""Change value of homematic system variable."""
|
"""Change value of homematic system variable."""
|
||||||
data = {
|
data = {
|
||||||
ATTR_ENTITY_ID: entity_id,
|
ATTR_ENTITY_ID: entity_id,
|
||||||
ATTR_VALUE: value,
|
ATTR_VALUE: value,
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_SET_VALUE, data)
|
hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data)
|
||||||
|
|
||||||
|
|
||||||
|
def set_dev_value(hass, address, channel, param, value, proxy=None):
|
||||||
|
"""Send virtual keypress to homematic controlller."""
|
||||||
|
data = {
|
||||||
|
ATTR_ADDRESS: address,
|
||||||
|
ATTR_CHANNEL: channel,
|
||||||
|
ATTR_PARAM: param,
|
||||||
|
ATTR_VALUE: value,
|
||||||
|
ATTR_PROXY: proxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data)
|
||||||
|
|
||||||
|
|
||||||
|
def reconnect(hass):
|
||||||
|
"""Reconnect to CCU/Homegear."""
|
||||||
|
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the Homematic component."""
|
"""Setup the Homematic component."""
|
||||||
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
|
||||||
from pyhomematic import HMConnection
|
from pyhomematic import HMConnection
|
||||||
|
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
local_ip = config[DOMAIN].get(CONF_LOCAL_IP)
|
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
|
||||||
local_port = config[DOMAIN].get(CONF_LOCAL_PORT)
|
hass.data[DATA_DEVINIT] = {}
|
||||||
remote_ip = config[DOMAIN].get(CONF_REMOTE_IP)
|
hass.data[DATA_STORE] = []
|
||||||
remote_port = config[DOMAIN].get(CONF_REMOTE_PORT)
|
|
||||||
resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES)
|
|
||||||
username = config[DOMAIN].get(CONF_USERNAME)
|
|
||||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
|
||||||
HOMEMATIC_LINK_DELAY = config[DOMAIN].get(CONF_DELAY)
|
|
||||||
use_variables = config[DOMAIN].get(CONF_VARIABLES)
|
|
||||||
|
|
||||||
if remote_ip is None or local_ip is None:
|
# create hosts list for pyhomematic
|
||||||
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
remotes = {}
|
||||||
return False
|
hosts = {}
|
||||||
|
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
|
||||||
|
server = rconfig.get(CONF_IP)
|
||||||
|
|
||||||
|
remotes[rname] = {}
|
||||||
|
remotes[rname][CONF_IP] = server
|
||||||
|
remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT)
|
||||||
|
remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES)
|
||||||
|
remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME)
|
||||||
|
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
if server not in hosts or rconfig.get(CONF_PRIMARY):
|
||||||
|
hosts[server] = {
|
||||||
|
CONF_VARIABLES: rconfig.get(CONF_VARIABLES),
|
||||||
|
CONF_NAME: rname,
|
||||||
|
}
|
||||||
|
hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES)
|
||||||
|
|
||||||
# Create server thread
|
# Create server thread
|
||||||
bound_system_callback = partial(_system_callback_handler, hass, config)
|
bound_system_callback = partial(_system_callback_handler, hass, config)
|
||||||
HOMEMATIC = HMConnection(local=local_ip,
|
hass.data[DATA_HOMEMATIC] = HMConnection(
|
||||||
localport=local_port,
|
local=config[DOMAIN].get(CONF_LOCAL_IP),
|
||||||
remote=remote_ip,
|
localport=config[DOMAIN].get(CONF_LOCAL_PORT),
|
||||||
remoteport=remote_port,
|
remotes=remotes,
|
||||||
systemcallback=bound_system_callback,
|
systemcallback=bound_system_callback,
|
||||||
resolvenames=resolvenames,
|
interface_id="homeassistant"
|
||||||
rpcusername=username,
|
)
|
||||||
rpcpassword=password,
|
|
||||||
interface_id="homeassistant")
|
|
||||||
|
|
||||||
# Start server thread, connect to peer, initialize to receive events
|
# Start server thread, connect to peer, initialize to receive events
|
||||||
HOMEMATIC.start()
|
hass.data[DATA_HOMEMATIC].start()
|
||||||
|
|
||||||
# Stops server when Homeassistant is shutting down
|
# Stops server when Homeassistant is shutting down
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
|
hass.bus.listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop)
|
||||||
hass.config.components.append(DOMAIN)
|
hass.config.components.append(DOMAIN)
|
||||||
|
|
||||||
|
# init homematic hubs
|
||||||
|
hub_entities = []
|
||||||
|
for _, hub_data in hosts.items():
|
||||||
|
hub_entities.append(HMHub(hass, component, hub_data[CONF_NAME],
|
||||||
|
hub_data[CONF_VARIABLES]))
|
||||||
|
component.add_entities(hub_entities)
|
||||||
|
|
||||||
# regeister homematic services
|
# regeister homematic services
|
||||||
descriptions = load_yaml_config_file(
|
descriptions = load_yaml_config_file(
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
|
def _hm_service_virtualkey(service):
|
||||||
_hm_service_virtualkey,
|
"""Service handle virtualkey services."""
|
||||||
|
address = service.data.get(ATTR_ADDRESS)
|
||||||
|
channel = service.data.get(ATTR_CHANNEL)
|
||||||
|
param = service.data.get(ATTR_PARAM)
|
||||||
|
|
||||||
|
# device not found
|
||||||
|
hmdevice = _device_from_servicecall(hass, service)
|
||||||
|
if hmdevice is None:
|
||||||
|
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||||
|
return
|
||||||
|
|
||||||
|
# if param exists for this device
|
||||||
|
if param not in hmdevice.ACTIONNODE:
|
||||||
|
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||||
|
return
|
||||||
|
|
||||||
|
# channel exists?
|
||||||
|
if channel not in hmdevice.ACTIONNODE[param]:
|
||||||
|
_LOGGER.error("%i is not a channel in hm device %s",
|
||||||
|
channel, address)
|
||||||
|
return
|
||||||
|
|
||||||
|
# call key
|
||||||
|
hmdevice.actionNodeData(param, True, channel)
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
|
||||||
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
||||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||||
|
|
||||||
entities = []
|
|
||||||
|
|
||||||
##
|
|
||||||
# init HM variable
|
|
||||||
variables = HOMEMATIC.getAllSystemVariables() if use_variables else {}
|
|
||||||
hm_var_store = {}
|
|
||||||
if variables is not None:
|
|
||||||
for key, value in variables.items():
|
|
||||||
varia = HMVariable(key, value)
|
|
||||||
hm_var_store.update({key: varia})
|
|
||||||
entities.append(varia)
|
|
||||||
|
|
||||||
# add homematic entites
|
|
||||||
entities.append(HMHub(hm_var_store, use_variables))
|
|
||||||
component.add_entities(entities)
|
|
||||||
|
|
||||||
##
|
|
||||||
# register set_value service if exists variables
|
|
||||||
if not variables:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _service_handle_value(service):
|
def _service_handle_value(service):
|
||||||
"""Set value on homematic variable object."""
|
"""Set value on homematic variable object."""
|
||||||
variable_list = component.extract_from_service(service)
|
variable_list = component.extract_from_service(service)
|
||||||
|
@ -261,12 +327,43 @@ def setup(hass, config):
|
||||||
value = service.data[ATTR_VALUE]
|
value = service.data[ATTR_VALUE]
|
||||||
|
|
||||||
for hm_variable in variable_list:
|
for hm_variable in variable_list:
|
||||||
|
if isinstance(hm_variable, HMVariable):
|
||||||
hm_variable.hm_set(value)
|
hm_variable.hm_set(value)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_SET_VALUE,
|
hass.services.register(
|
||||||
_service_handle_value,
|
DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value,
|
||||||
descriptions[DOMAIN][SERVICE_SET_VALUE],
|
descriptions[DOMAIN][SERVICE_SET_VAR_VALUE],
|
||||||
schema=SCHEMA_SERVICE_SET_VALUE)
|
schema=SCHEMA_SERVICE_SET_VAR_VALUE)
|
||||||
|
|
||||||
|
def _service_handle_reconnect(service):
|
||||||
|
"""Reconnect to all homematic hubs."""
|
||||||
|
hass.data[DATA_HOMEMATIC].reconnect()
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
|
||||||
|
descriptions[DOMAIN][SERVICE_RECONNECT],
|
||||||
|
schema=SCHEMA_SERVICE_RECONNECT)
|
||||||
|
|
||||||
|
def _service_handle_device(service):
|
||||||
|
"""Service handle set_dev_value services."""
|
||||||
|
address = service.data.get(ATTR_ADDRESS)
|
||||||
|
channel = service.data.get(ATTR_CHANNEL)
|
||||||
|
param = service.data.get(ATTR_PARAM)
|
||||||
|
value = service.data.get(ATTR_VALUE)
|
||||||
|
|
||||||
|
# device not found
|
||||||
|
hmdevice = _device_from_servicecall(hass, service)
|
||||||
|
if hmdevice is None:
|
||||||
|
_LOGGER.error("%s not found!", address)
|
||||||
|
return
|
||||||
|
|
||||||
|
# call key
|
||||||
|
hmdevice.setValue(param, value, channel)
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device,
|
||||||
|
descriptions[DOMAIN][SERVICE_SET_DEV_VALUE],
|
||||||
|
schema=SCHEMA_SERVICE_SET_DEV_VALUE)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -274,22 +371,36 @@ def setup(hass, config):
|
||||||
def _system_callback_handler(hass, config, src, *args):
|
def _system_callback_handler(hass, config, src, *args):
|
||||||
"""Callback handler."""
|
"""Callback handler."""
|
||||||
if src == 'newDevices':
|
if src == 'newDevices':
|
||||||
_LOGGER.debug("newDevices with: %s", str(args))
|
_LOGGER.debug("newDevices with: %s", args)
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
(interface_id, dev_descriptions) = args
|
(interface_id, dev_descriptions) = args
|
||||||
key_dict = {}
|
proxy = interface_id.split('-')[-1]
|
||||||
|
|
||||||
|
# device support active?
|
||||||
|
if not hass.data[DATA_DEVINIT][proxy]:
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
# Get list of all keys of the devices (ignoring channels)
|
# Get list of all keys of the devices (ignoring channels)
|
||||||
|
key_dict = {}
|
||||||
for dev in dev_descriptions:
|
for dev in dev_descriptions:
|
||||||
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
||||||
|
|
||||||
|
##
|
||||||
|
# remove device they allready init by HA
|
||||||
|
tmp_devs = key_dict.copy()
|
||||||
|
for dev in tmp_devs:
|
||||||
|
if dev in hass.data[DATA_STORE]:
|
||||||
|
del key_dict[dev]
|
||||||
|
else:
|
||||||
|
hass.data[DATA_STORE].append(dev)
|
||||||
|
|
||||||
# Register EVENTS
|
# Register EVENTS
|
||||||
# Search all device with a EVENTNODE that include data
|
# Search all device with a EVENTNODE that include data
|
||||||
bound_event_callback = partial(_hm_event_handler, hass)
|
bound_event_callback = partial(_hm_event_handler, hass, proxy)
|
||||||
for dev in key_dict:
|
for dev in key_dict:
|
||||||
if dev not in HOMEMATIC.devices:
|
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
|
||||||
continue
|
|
||||||
|
|
||||||
hmdevice = HOMEMATIC.devices.get(dev)
|
|
||||||
# have events?
|
# have events?
|
||||||
if len(hmdevice.EVENTNODE) > 0:
|
if len(hmdevice.EVENTNODE) > 0:
|
||||||
_LOGGER.debug("Register Events from %s", dev)
|
_LOGGER.debug("Register Events from %s", dev)
|
||||||
|
@ -307,7 +418,8 @@ def _system_callback_handler(hass, config, src, *args):
|
||||||
('sensor', DISCOVER_SENSORS),
|
('sensor', DISCOVER_SENSORS),
|
||||||
('climate', DISCOVER_CLIMATE)):
|
('climate', DISCOVER_CLIMATE)):
|
||||||
# Get all devices of a specific type
|
# Get all devices of a specific type
|
||||||
found_devices = _get_devices(discovery_type, key_dict)
|
found_devices = _get_devices(
|
||||||
|
hass, discovery_type, key_dict, proxy)
|
||||||
|
|
||||||
# When devices of this type are found
|
# When devices of this type are found
|
||||||
# they are setup in HA and an event is fired
|
# they are setup in HA and an event is fired
|
||||||
|
@ -318,12 +430,12 @@ def _system_callback_handler(hass, config, src, *args):
|
||||||
}, config)
|
}, config)
|
||||||
|
|
||||||
|
|
||||||
def _get_devices(device_type, keys):
|
def _get_devices(hass, device_type, keys, proxy):
|
||||||
"""Get the Homematic devices."""
|
"""Get the Homematic devices."""
|
||||||
device_arr = []
|
device_arr = []
|
||||||
|
|
||||||
for key in keys:
|
for key in keys:
|
||||||
device = HOMEMATIC.devices[key]
|
device = hass.data[DATA_HOMEMATIC].devices[proxy][key]
|
||||||
class_name = device.__class__.__name__
|
class_name = device.__class__.__name__
|
||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
||||||
|
@ -357,6 +469,7 @@ def _get_devices(device_type, keys):
|
||||||
device_dict = {
|
device_dict = {
|
||||||
CONF_PLATFORM: "homematic",
|
CONF_PLATFORM: "homematic",
|
||||||
ATTR_ADDRESS: key,
|
ATTR_ADDRESS: key,
|
||||||
|
ATTR_PROXY: proxy,
|
||||||
ATTR_NAME: name,
|
ATTR_NAME: name,
|
||||||
ATTR_CHANNEL: channel
|
ATTR_CHANNEL: channel
|
||||||
}
|
}
|
||||||
|
@ -395,28 +508,29 @@ def _create_ha_name(name, channel, param, count):
|
||||||
return "{} {} {}".format(name, channel, param)
|
return "{} {} {}".format(name, channel, param)
|
||||||
|
|
||||||
|
|
||||||
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
|
def setup_hmdevice_discovery_helper(hass, hmdevicetype, discovery_info,
|
||||||
add_callback_devices):
|
add_callback_devices):
|
||||||
"""Helper to setup Homematic devices with discovery info."""
|
"""Helper to setup Homematic devices with discovery info."""
|
||||||
|
devices = []
|
||||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||||
_LOGGER.debug("Add device %s from config: %s",
|
_LOGGER.debug("Add device %s from config: %s",
|
||||||
str(hmdevicetype), str(config))
|
str(hmdevicetype), str(config))
|
||||||
|
|
||||||
# create object and add to HA
|
# create object and add to HA
|
||||||
new_device = hmdevicetype(config)
|
new_device = hmdevicetype(hass, config)
|
||||||
new_device.link_homematic()
|
new_device.link_homematic()
|
||||||
|
devices.append(new_device)
|
||||||
|
|
||||||
add_callback_devices([new_device])
|
add_callback_devices(devices)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _hm_event_handler(hass, device, caller, attribute, value):
|
def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||||
"""Handle all pyhomematic device events."""
|
"""Handle all pyhomematic device events."""
|
||||||
try:
|
try:
|
||||||
channel = int(device.split(":")[1])
|
channel = int(device.split(":")[1])
|
||||||
address = device.split(":")[0]
|
address = device.split(":")[0]
|
||||||
hmdevice = HOMEMATIC.devices.get(address)
|
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
_LOGGER.error("Event handling channel convert error!")
|
_LOGGER.error("Event handling channel convert error!")
|
||||||
return
|
return
|
||||||
|
@ -448,46 +562,40 @@ def _hm_event_handler(hass, device, caller, attribute, value):
|
||||||
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
||||||
|
|
||||||
|
|
||||||
def _hm_service_virtualkey(call):
|
def _device_from_servicecall(hass, service):
|
||||||
"""Callback for handle virtualkey services."""
|
"""Extract homematic device from service call."""
|
||||||
address = call.data.get(ATTR_ADDRESS)
|
address = service.data.get(ATTR_ADDRESS)
|
||||||
channel = call.data.get(ATTR_CHANNEL)
|
proxy = service.data.get(ATTR_PROXY)
|
||||||
param = call.data.get(ATTR_PARAM)
|
|
||||||
|
|
||||||
if address not in HOMEMATIC.devices:
|
if proxy:
|
||||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
return hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
|
||||||
return
|
|
||||||
hmdevice = HOMEMATIC.devices.get(address)
|
|
||||||
|
|
||||||
# if param exists for this device
|
for _, devices in hass.data[DATA_HOMEMATIC].devices.items():
|
||||||
if hmdevice is None or param not in hmdevice.ACTIONNODE:
|
if address in devices:
|
||||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
return devices[address]
|
||||||
return
|
|
||||||
|
|
||||||
# channel exists?
|
|
||||||
if channel in hmdevice.ACTIONNODE[param]:
|
|
||||||
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
|
|
||||||
return
|
|
||||||
|
|
||||||
# call key
|
|
||||||
hmdevice.actionNodeData(param, 1, channel)
|
|
||||||
|
|
||||||
|
|
||||||
class HMHub(Entity):
|
class HMHub(Entity):
|
||||||
"""The Homematic hub. I.e. CCU2/HomeGear."""
|
"""The Homematic hub. I.e. CCU2/HomeGear."""
|
||||||
|
|
||||||
def __init__(self, variables_store, use_variables=False):
|
def __init__(self, hass, component, name, use_variables):
|
||||||
"""Initialize Homematic hub."""
|
"""Initialize Homematic hub."""
|
||||||
|
self.hass = hass
|
||||||
|
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||||
|
self._component = component
|
||||||
|
self._name = name
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._store = variables_store
|
self._store = {}
|
||||||
self._use_variables = use_variables
|
self._use_variables = use_variables
|
||||||
|
|
||||||
self.update()
|
# load data
|
||||||
|
self._update_hub_state()
|
||||||
|
self._init_variables()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return 'Homematic'
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
@ -504,11 +612,6 @@ class HMHub(Entity):
|
||||||
"""Return the icon to use in the frontend, if any."""
|
"""Return the icon to use in the frontend, if any."""
|
||||||
return "mdi:gradient"
|
return "mdi:gradient"
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return true if device is available."""
|
|
||||||
return True if HOMEMATIC is not None else False
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update Hub data and all HM variables."""
|
"""Update Hub data and all HM variables."""
|
||||||
self._update_hub_state()
|
self._update_hub_state()
|
||||||
|
@ -517,30 +620,48 @@ class HMHub(Entity):
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
|
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
|
||||||
def _update_hub_state(self):
|
def _update_hub_state(self):
|
||||||
"""Retrieve latest state."""
|
"""Retrieve latest state."""
|
||||||
if HOMEMATIC is None:
|
state = self._homematic.getServiceMessages(self._name)
|
||||||
return
|
|
||||||
state = HOMEMATIC.getServiceMessages()
|
|
||||||
self._state = STATE_UNKNOWN if state is None else len(state)
|
self._state = STATE_UNKNOWN if state is None else len(state)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
|
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
|
||||||
def _update_variables_state(self):
|
def _update_variables_state(self):
|
||||||
"""Retrive all variable data and update hmvariable states."""
|
"""Retrive all variable data and update hmvariable states."""
|
||||||
if HOMEMATIC is None or not self._use_variables:
|
if not self._use_variables:
|
||||||
return
|
return
|
||||||
variables = HOMEMATIC.getAllSystemVariables()
|
|
||||||
if variables is not None:
|
variables = self._homematic.getAllSystemVariables(self._name)
|
||||||
|
if variables is None:
|
||||||
|
return
|
||||||
|
|
||||||
for key, value in variables.items():
|
for key, value in variables.items():
|
||||||
if key in self._store:
|
if key in self._store:
|
||||||
self._store.get(key).hm_update(value)
|
self._store.get(key).hm_update(value)
|
||||||
|
|
||||||
|
def _init_variables(self):
|
||||||
|
"""Load variables from hub."""
|
||||||
|
if not self._use_variables:
|
||||||
|
return
|
||||||
|
|
||||||
|
variables = self._homematic.getAllSystemVariables(self._name)
|
||||||
|
if variables is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
for key, value in variables.items():
|
||||||
|
entities.append(HMVariable(self.hass, self._name, key, value))
|
||||||
|
self._component.add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class HMVariable(Entity):
|
class HMVariable(Entity):
|
||||||
"""The Homematic system variable."""
|
"""The Homematic system variable."""
|
||||||
|
|
||||||
def __init__(self, name, state):
|
def __init__(self, hass, hub_name, name, state):
|
||||||
"""Initialize Homematic hub."""
|
"""Initialize Homematic hub."""
|
||||||
|
self.hass = hass
|
||||||
|
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||||
self._state = state
|
self._state = state
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._hub_name = hub_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -562,31 +683,41 @@ class HMVariable(Entity):
|
||||||
"""Return false. Homematic Hub object update variable."""
|
"""Return false. Homematic Hub object update variable."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return device specific state attributes."""
|
||||||
|
attr = {
|
||||||
|
'hub': self._hub_name,
|
||||||
|
}
|
||||||
|
return attr
|
||||||
|
|
||||||
def hm_update(self, value):
|
def hm_update(self, value):
|
||||||
"""Update variable over Hub object."""
|
"""Update variable over Hub object."""
|
||||||
if value != self._state:
|
if value != self._state:
|
||||||
self._state = value
|
self._state = value
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def hm_set(self, value):
|
def hm_set(self, value):
|
||||||
"""Set variable on homematic controller."""
|
"""Set variable on homematic controller."""
|
||||||
if HOMEMATIC is not None:
|
|
||||||
if isinstance(self._state, bool):
|
if isinstance(self._state, bool):
|
||||||
value = cv.boolean(value)
|
value = cv.boolean(value)
|
||||||
else:
|
else:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
HOMEMATIC.setSystemVariable(self._name, value)
|
self._homematic.setSystemVariable(self._hub_name, self._name, value)
|
||||||
self._state = value
|
self._state = value
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class HMDevice(Entity):
|
class HMDevice(Entity):
|
||||||
"""The Homematic device base object."""
|
"""The Homematic device base object."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize a generic Homematic device."""
|
"""Initialize a generic Homematic device."""
|
||||||
|
self.hass = hass
|
||||||
|
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||||
self._name = config.get(ATTR_NAME)
|
self._name = config.get(ATTR_NAME)
|
||||||
self._address = config.get(ATTR_ADDRESS)
|
self._address = config.get(ATTR_ADDRESS)
|
||||||
|
self._proxy = config.get(ATTR_PROXY)
|
||||||
self._channel = config.get(ATTR_CHANNEL)
|
self._channel = config.get(ATTR_CHANNEL)
|
||||||
self._state = config.get(ATTR_PARAM)
|
self._state = config.get(ATTR_PARAM)
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
@ -636,6 +767,7 @@ class HMDevice(Entity):
|
||||||
|
|
||||||
# static attributes
|
# static attributes
|
||||||
attr['ID'] = self._hmdevice.ADDRESS
|
attr['ID'] = self._hmdevice.ADDRESS
|
||||||
|
attr['proxy'] = self._proxy
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
|
@ -645,14 +777,8 @@ class HMDevice(Entity):
|
||||||
if self._connected:
|
if self._connected:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# pyhomematic is loaded
|
|
||||||
if HOMEMATIC is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does a HMDevice from pyhomematic exist?
|
|
||||||
if self._address in HOMEMATIC.devices:
|
|
||||||
# Init
|
# Init
|
||||||
self._hmdevice = HOMEMATIC.devices[self._address]
|
self._hmdevice = self._homematic.devices[self._proxy][self._address]
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
# Check if Homematic class is okay for HA class
|
# Check if Homematic class is okay for HA class
|
||||||
|
@ -660,10 +786,10 @@ class HMDevice(Entity):
|
||||||
try:
|
try:
|
||||||
# Init datapoints of this object
|
# Init datapoints of this object
|
||||||
self._init_data()
|
self._init_data()
|
||||||
if HOMEMATIC_LINK_DELAY:
|
if self.hass.data[DATA_DELAY]:
|
||||||
# We delay / pause loading of data to avoid overloading
|
# We delay / pause loading of data to avoid overloading
|
||||||
# of CCU / Homegear when doing auto detection
|
# of CCU / Homegear when doing auto detection
|
||||||
time.sleep(HOMEMATIC_LINK_DELAY)
|
time.sleep(self.hass.data[DATA_DELAY])
|
||||||
self._load_data_from_hm()
|
self._load_data_from_hm()
|
||||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||||
|
|
||||||
|
@ -676,8 +802,6 @@ class HMDevice(Entity):
|
||||||
self._connected = False
|
self._connected = False
|
||||||
_LOGGER.error("Exception while linking %s: %s",
|
_LOGGER.error("Exception while linking %s: %s",
|
||||||
self._address, str(err))
|
self._address, str(err))
|
||||||
else:
|
|
||||||
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
|
|
||||||
|
|
||||||
def _hm_event_callback(self, device, caller, attribute, value):
|
def _hm_event_callback(self, device, caller, attribute, value):
|
||||||
"""Handle all pyhomematic device events."""
|
"""Handle all pyhomematic device events."""
|
||||||
|
|
|
@ -5,35 +5,38 @@ For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/http/
|
https://home-assistant.io/components/http/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import hmac
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import ssl
|
import ssl
|
||||||
from ipaddress import ip_address, ip_network
|
from ipaddress import ip_network
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import os
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from aiohttp import web, hdrs
|
from aiohttp import web
|
||||||
from aiohttp.file_sender import FileSender
|
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
|
||||||
from aiohttp.web_exceptions import (
|
|
||||||
HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified)
|
|
||||||
from aiohttp.web_urldispatcher import StaticRoute
|
|
||||||
|
|
||||||
from homeassistant.core import is_callback
|
|
||||||
import homeassistant.remote as rem
|
|
||||||
from homeassistant import util
|
|
||||||
from homeassistant.const import (
|
|
||||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
|
|
||||||
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
|
|
||||||
EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.remote as rem
|
||||||
|
from homeassistant.util import get_local_ip
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
|
from homeassistant.const import (
|
||||||
|
SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS,
|
||||||
|
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||||
|
from homeassistant.core import is_callback
|
||||||
|
from homeassistant.util.logging import HideSensitiveDataFilter
|
||||||
|
|
||||||
|
from .auth import auth_middleware
|
||||||
|
from .ban import ban_middleware, process_wrong_login
|
||||||
|
from .const import (
|
||||||
|
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
|
||||||
|
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
|
||||||
|
KEY_DEVELOPMENT, KEY_AUTHENTICATED)
|
||||||
|
from .static import GZIP_FILE_SENDER, staticresource_middleware
|
||||||
|
from .util import get_real_ip
|
||||||
|
|
||||||
DOMAIN = 'http'
|
DOMAIN = 'http'
|
||||||
REQUIREMENTS = ('aiohttp_cors==0.4.0',)
|
REQUIREMENTS = ('aiohttp_cors==0.5.0',)
|
||||||
|
|
||||||
CONF_API_PASSWORD = 'api_password'
|
CONF_API_PASSWORD = 'api_password'
|
||||||
CONF_SERVER_HOST = 'server_host'
|
CONF_SERVER_HOST = 'server_host'
|
||||||
|
@ -44,8 +47,9 @@ CONF_SSL_KEY = 'ssl_key'
|
||||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||||
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
||||||
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||||
|
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
|
||||||
|
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
|
||||||
|
|
||||||
DATA_API_PASSWORD = 'api_password'
|
|
||||||
NOTIFICATION_ID_LOGIN = 'http-login'
|
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||||
|
|
||||||
# TLS configuation follows the best-practice guidelines specified here:
|
# TLS configuation follows the best-practice guidelines specified here:
|
||||||
|
@ -69,68 +73,58 @@ CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
|
||||||
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
|
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
|
||||||
"AES256-SHA:DES-CBC3-SHA:!DSS"
|
"AES256-SHA:DES-CBC3-SHA:!DSS"
|
||||||
|
|
||||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
DEFAULT_SERVER_HOST = '0.0.0.0'
|
||||||
DOMAIN: vol.Schema({
|
DEFAULT_DEVELOPMENT = '0'
|
||||||
vol.Optional(CONF_API_PASSWORD): cv.string,
|
DEFAULT_LOGIN_ATTEMPT_THRESHOLD = -1
|
||||||
vol.Optional(CONF_SERVER_HOST): cv.string,
|
|
||||||
|
HTTP_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_API_PASSWORD, default=None): cv.string,
|
||||||
|
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
|
||||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||||
vol.Optional(CONF_DEVELOPMENT): cv.string,
|
vol.Optional(CONF_DEVELOPMENT, default=DEFAULT_DEVELOPMENT): cv.string,
|
||||||
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile,
|
||||||
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
vol.Optional(CONF_SSL_KEY, default=None): cv.isfile,
|
||||||
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list,
|
||||||
|
[cv.string]),
|
||||||
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
|
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_TRUSTED_NETWORKS):
|
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
|
||||||
vol.All(cv.ensure_list, [ip_network])
|
vol.All(cv.ensure_list, [ip_network]),
|
||||||
}),
|
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
|
||||||
|
default=DEFAULT_LOGIN_ATTEMPT_THRESHOLD): cv.positive_int,
|
||||||
|
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: HTTP_SCHEMA,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# TEMP TO GET TESTS TO RUN
|
@asyncio.coroutine
|
||||||
def request_class():
|
def async_setup(hass, config):
|
||||||
"""."""
|
|
||||||
raise Exception('not implemented')
|
|
||||||
|
|
||||||
|
|
||||||
class HideSensitiveFilter(logging.Filter):
|
|
||||||
"""Filter API password calls."""
|
|
||||||
|
|
||||||
def __init__(self, hass):
|
|
||||||
"""Initialize sensitive data filter."""
|
|
||||||
super().__init__()
|
|
||||||
self.hass = hass
|
|
||||||
|
|
||||||
def filter(self, record):
|
|
||||||
"""Hide sensitive data in messages."""
|
|
||||||
if self.hass.http.api_password is None:
|
|
||||||
return True
|
|
||||||
|
|
||||||
record.msg = record.msg.replace(self.hass.http.api_password, '*******')
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Set up the HTTP API and debug interface."""
|
"""Set up the HTTP API and debug interface."""
|
||||||
logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass))
|
conf = config.get(DOMAIN)
|
||||||
|
|
||||||
conf = config.get(DOMAIN, {})
|
if conf is None:
|
||||||
|
conf = HTTP_SCHEMA({})
|
||||||
|
|
||||||
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
api_password = conf[CONF_API_PASSWORD]
|
||||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
server_host = conf[CONF_SERVER_HOST]
|
||||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
server_port = conf[CONF_SERVER_PORT]
|
||||||
development = str(conf.get(CONF_DEVELOPMENT, '')) == '1'
|
development = conf[CONF_DEVELOPMENT] == '1'
|
||||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
ssl_certificate = conf[CONF_SSL_CERTIFICATE]
|
||||||
ssl_key = conf.get(CONF_SSL_KEY)
|
ssl_key = conf[CONF_SSL_KEY]
|
||||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
cors_origins = conf[CONF_CORS_ORIGINS]
|
||||||
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
|
||||||
trusted_networks = [
|
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
|
||||||
ip_network(trusted_network)
|
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
||||||
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
|
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
||||||
|
|
||||||
|
if api_password is not None:
|
||||||
|
logging.getLogger('aiohttp.access').addFilter(
|
||||||
|
HideSensitiveDataFilter(api_password))
|
||||||
|
|
||||||
server = HomeAssistantWSGI(
|
server = HomeAssistantWSGI(
|
||||||
hass,
|
hass,
|
||||||
|
@ -142,7 +136,9 @@ def setup(hass, config):
|
||||||
ssl_key=ssl_key,
|
ssl_key=ssl_key,
|
||||||
cors_origins=cors_origins,
|
cors_origins=cors_origins,
|
||||||
use_x_forwarded_for=use_x_forwarded_for,
|
use_x_forwarded_for=use_x_forwarded_for,
|
||||||
trusted_networks=trusted_networks
|
trusted_networks=trusted_networks,
|
||||||
|
login_threshold=login_threshold,
|
||||||
|
is_ban_enabled=is_ban_enabled
|
||||||
)
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -156,108 +152,40 @@ def setup(hass, config):
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
|
||||||
yield from server.start()
|
yield from server.start()
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
|
||||||
|
|
||||||
hass.http = server
|
hass.http = server
|
||||||
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||||
else util.get_local_ip(),
|
else get_local_ip(),
|
||||||
api_password, server_port,
|
api_password, server_port,
|
||||||
ssl_certificate is not None)
|
ssl_certificate is not None)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class GzipFileSender(FileSender):
|
|
||||||
"""FileSender class capable of sending gzip version if available."""
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
|
|
||||||
development = False
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def send(self, request, filepath):
|
|
||||||
"""Send filepath to client using request."""
|
|
||||||
gzip = False
|
|
||||||
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
|
|
||||||
gzip_path = filepath.with_name(filepath.name + '.gz')
|
|
||||||
|
|
||||||
if gzip_path.is_file():
|
|
||||||
filepath = gzip_path
|
|
||||||
gzip = True
|
|
||||||
|
|
||||||
st = filepath.stat()
|
|
||||||
|
|
||||||
modsince = request.if_modified_since
|
|
||||||
if modsince is not None and st.st_mtime <= modsince.timestamp():
|
|
||||||
raise HTTPNotModified()
|
|
||||||
|
|
||||||
ct, encoding = mimetypes.guess_type(str(filepath))
|
|
||||||
if not ct:
|
|
||||||
ct = 'application/octet-stream'
|
|
||||||
|
|
||||||
resp = self._response_factory()
|
|
||||||
resp.content_type = ct
|
|
||||||
if encoding:
|
|
||||||
resp.headers[hdrs.CONTENT_ENCODING] = encoding
|
|
||||||
if gzip:
|
|
||||||
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
|
|
||||||
resp.last_modified = st.st_mtime
|
|
||||||
|
|
||||||
# CACHE HACK
|
|
||||||
if not self.development:
|
|
||||||
cache_time = 31 * 86400 # = 1 month
|
|
||||||
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
|
|
||||||
cache_time)
|
|
||||||
|
|
||||||
file_size = st.st_size
|
|
||||||
|
|
||||||
resp.content_length = file_size
|
|
||||||
resp.set_tcp_cork(True)
|
|
||||||
try:
|
|
||||||
with filepath.open('rb') as f:
|
|
||||||
yield from self._sendfile(request, resp, f, file_size)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
resp.set_tcp_nodelay(True)
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
_GZIP_FILE_SENDER = GzipFileSender()
|
|
||||||
|
|
||||||
|
|
||||||
class HAStaticRoute(StaticRoute):
|
|
||||||
"""StaticRoute with support for fingerprinting."""
|
|
||||||
|
|
||||||
def __init__(self, prefix, path):
|
|
||||||
"""Initialize a static route with gzip and cache busting support."""
|
|
||||||
super().__init__(None, prefix, path)
|
|
||||||
self._file_sender = _GZIP_FILE_SENDER
|
|
||||||
|
|
||||||
def match(self, path):
|
|
||||||
"""Match path to filename."""
|
|
||||||
if not path.startswith(self._prefix):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Extra sauce to remove fingerprinted resource names
|
|
||||||
filename = path[self._prefix_len:]
|
|
||||||
fingerprinted = _FINGERPRINT.match(filename)
|
|
||||||
if fingerprinted:
|
|
||||||
filename = '{}.{}'.format(*fingerprinted.groups())
|
|
||||||
|
|
||||||
return {'filename': filename}
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantWSGI(object):
|
class HomeAssistantWSGI(object):
|
||||||
"""WSGI server for Home Assistant."""
|
"""WSGI server for Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||||
ssl_key, server_host, server_port, cors_origins,
|
ssl_key, server_host, server_port, cors_origins,
|
||||||
use_x_forwarded_for, trusted_networks):
|
use_x_forwarded_for, trusted_networks,
|
||||||
|
login_threshold, is_ban_enabled):
|
||||||
"""Initialize the WSGI Home Assistant server."""
|
"""Initialize the WSGI Home Assistant server."""
|
||||||
import aiohttp_cors
|
import aiohttp_cors
|
||||||
|
|
||||||
self.app = web.Application(loop=hass.loop)
|
middlewares = [auth_middleware, staticresource_middleware]
|
||||||
|
|
||||||
|
if is_ban_enabled:
|
||||||
|
middlewares.insert(0, ban_middleware)
|
||||||
|
|
||||||
|
self.app = web.Application(middlewares=middlewares, loop=hass.loop)
|
||||||
|
self.app['hass'] = hass
|
||||||
|
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
|
||||||
|
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
|
||||||
|
self.app[KEY_BANS_ENABLED] = is_ban_enabled
|
||||||
|
self.app[KEY_LOGIN_THRESHOLD] = login_threshold
|
||||||
|
self.app[KEY_DEVELOPMENT] = development
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.development = development
|
self.development = development
|
||||||
self.api_password = api_password
|
self.api_password = api_password
|
||||||
|
@ -265,9 +193,6 @@ class HomeAssistantWSGI(object):
|
||||||
self.ssl_key = ssl_key
|
self.ssl_key = ssl_key
|
||||||
self.server_host = server_host
|
self.server_host = server_host
|
||||||
self.server_port = server_port
|
self.server_port = server_port
|
||||||
self.use_x_forwarded_for = use_x_forwarded_for
|
|
||||||
self.trusted_networks = trusted_networks
|
|
||||||
self.event_forwarder = None
|
|
||||||
self._handler = None
|
self._handler = None
|
||||||
self.server = None
|
self.server = None
|
||||||
|
|
||||||
|
@ -281,9 +206,6 @@ class HomeAssistantWSGI(object):
|
||||||
else:
|
else:
|
||||||
self.cors = None
|
self.cors = None
|
||||||
|
|
||||||
# CACHE HACK
|
|
||||||
_GZIP_FILE_SENDER.development = development
|
|
||||||
|
|
||||||
def register_view(self, view):
|
def register_view(self, view):
|
||||||
"""Register a view with the WSGI server.
|
"""Register a view with the WSGI server.
|
||||||
|
|
||||||
|
@ -293,7 +215,19 @@ class HomeAssistantWSGI(object):
|
||||||
"""
|
"""
|
||||||
if isinstance(view, type):
|
if isinstance(view, type):
|
||||||
# Instantiate the view, if needed
|
# Instantiate the view, if needed
|
||||||
view = view(self.hass)
|
view = view()
|
||||||
|
|
||||||
|
if not hasattr(view, 'url'):
|
||||||
|
class_name = view.__class__.__name__
|
||||||
|
raise AttributeError(
|
||||||
|
'{0} missing required attribute "url"'.format(class_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not hasattr(view, 'name'):
|
||||||
|
class_name = view.__class__.__name__
|
||||||
|
raise AttributeError(
|
||||||
|
'{0} missing required attribute "name"'.format(class_name)
|
||||||
|
)
|
||||||
|
|
||||||
view.register(self.app.router)
|
view.register(self.app.router)
|
||||||
|
|
||||||
|
@ -318,19 +252,15 @@ class HomeAssistantWSGI(object):
|
||||||
Specify optional cache length of asset in days.
|
Specify optional cache length of asset in days.
|
||||||
"""
|
"""
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
assert url_root.startswith('/')
|
self.app.router.add_static(url_root, path)
|
||||||
if not url_root.endswith('/'):
|
|
||||||
url_root += '/'
|
|
||||||
route = HAStaticRoute(url_root, path)
|
|
||||||
self.app.router.register_route(route)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
filepath = Path(path)
|
filepath = Path(path)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def serve_file(request):
|
def serve_file(request):
|
||||||
"""Redirect to location."""
|
"""Serve file from disk."""
|
||||||
res = yield from _GZIP_FILE_SENDER.send(request, filepath)
|
res = yield from GZIP_FILE_SENDER.send(request, filepath)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# aiohttp supports regex matching for variables. Using that as temp
|
# aiohttp supports regex matching for variables. Using that as temp
|
||||||
|
@ -359,10 +289,21 @@ class HomeAssistantWSGI(object):
|
||||||
else:
|
else:
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
|
# Aiohttp freezes apps after start so that no changes can be made.
|
||||||
|
# However in Home Assistant components can be discovered after boot.
|
||||||
|
# This will now raise a RunTimeError.
|
||||||
|
# To work around this we now fake that we are frozen.
|
||||||
|
# A more appropriate fix would be to create a new app and
|
||||||
|
# re-register all redirects, views, static paths.
|
||||||
|
self.app._frozen = True # pylint: disable=protected-access
|
||||||
|
|
||||||
self._handler = self.app.make_handler()
|
self._handler = self.app.make_handler()
|
||||||
|
|
||||||
self.server = yield from self.hass.loop.create_server(
|
self.server = yield from self.hass.loop.create_server(
|
||||||
self._handler, self.server_host, self.server_port, ssl=context)
|
self._handler, self.server_host, self.server_port, ssl=context)
|
||||||
|
|
||||||
|
self.app._frozen = False # pylint: disable=protected-access
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the wsgi server."""
|
"""Stop the wsgi server."""
|
||||||
|
@ -372,21 +313,6 @@ class HomeAssistantWSGI(object):
|
||||||
yield from self._handler.finish_connections(60.0)
|
yield from self._handler.finish_connections(60.0)
|
||||||
yield from self.app.cleanup()
|
yield from self.app.cleanup()
|
||||||
|
|
||||||
def get_real_ip(self, request):
|
|
||||||
"""Return the clients correct ip address, even in proxied setups."""
|
|
||||||
if self.use_x_forwarded_for \
|
|
||||||
and HTTP_HEADER_X_FORWARDED_FOR in request.headers:
|
|
||||||
return request.headers.get(
|
|
||||||
HTTP_HEADER_X_FORWARDED_FOR).split(',')[0]
|
|
||||||
else:
|
|
||||||
peername = request.transport.get_extra_info('peername')
|
|
||||||
return peername[0] if peername is not None else None
|
|
||||||
|
|
||||||
def is_trusted_ip(self, remote_addr):
|
|
||||||
"""Match an ip address against trusted CIDR networks."""
|
|
||||||
return any(ip_address(remote_addr) in trusted_network
|
|
||||||
for trusted_network in self.hass.http.trusted_networks)
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantView(object):
|
class HomeAssistantView(object):
|
||||||
"""Base view for all views."""
|
"""Base view for all views."""
|
||||||
|
@ -395,22 +321,6 @@ class HomeAssistantView(object):
|
||||||
extra_urls = []
|
extra_urls = []
|
||||||
requires_auth = True # Views inheriting from this class can override this
|
requires_auth = True # Views inheriting from this class can override this
|
||||||
|
|
||||||
def __init__(self, hass):
|
|
||||||
"""Initilalize the base view."""
|
|
||||||
if not hasattr(self, 'url'):
|
|
||||||
class_name = self.__class__.__name__
|
|
||||||
raise AttributeError(
|
|
||||||
'{0} missing required attribute "url"'.format(class_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not hasattr(self, 'name'):
|
|
||||||
class_name = self.__class__.__name__
|
|
||||||
raise AttributeError(
|
|
||||||
'{0} missing required attribute "name"'.format(class_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass = hass
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
def json(self, result, status_code=200):
|
def json(self, result, status_code=200):
|
||||||
"""Return a JSON response."""
|
"""Return a JSON response."""
|
||||||
|
@ -428,7 +338,7 @@ class HomeAssistantView(object):
|
||||||
def file(self, request, fil):
|
def file(self, request, fil):
|
||||||
"""Return a file."""
|
"""Return a file."""
|
||||||
assert isinstance(fil, str), 'only string paths allowed'
|
assert isinstance(fil, str), 'only string paths allowed'
|
||||||
response = yield from _GZIP_FILE_SENDER.send(request, Path(fil))
|
response = yield from GZIP_FILE_SENDER.send(request, Path(fil))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def register(self, router):
|
def register(self, router):
|
||||||
|
@ -455,53 +365,32 @@ class HomeAssistantView(object):
|
||||||
|
|
||||||
|
|
||||||
def request_handler_factory(view, handler):
|
def request_handler_factory(view, handler):
|
||||||
"""Factory to wrap our handler classes.
|
"""Factory to wrap our handler classes."""
|
||||||
|
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
|
||||||
|
"Handler should be a coroutine or a callback."
|
||||||
|
|
||||||
Eventually authentication should be managed by middleware.
|
|
||||||
"""
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle(request):
|
def handle(request):
|
||||||
"""Handle incoming request."""
|
"""Handle incoming request."""
|
||||||
if not view.hass.is_running:
|
if not request.app['hass'].is_running:
|
||||||
return web.Response(status=503)
|
return web.Response(status=503)
|
||||||
|
|
||||||
remote_addr = view.hass.http.get_real_ip(request)
|
remote_addr = get_real_ip(request)
|
||||||
|
authenticated = request.get(KEY_AUTHENTICATED, False)
|
||||||
# Auth code verbose on purpose
|
|
||||||
authenticated = False
|
|
||||||
|
|
||||||
if view.hass.http.api_password is None:
|
|
||||||
authenticated = True
|
|
||||||
|
|
||||||
elif view.hass.http.is_trusted_ip(remote_addr):
|
|
||||||
authenticated = True
|
|
||||||
|
|
||||||
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
|
|
||||||
view.hass.http.api_password):
|
|
||||||
# A valid auth header has been set
|
|
||||||
authenticated = True
|
|
||||||
|
|
||||||
elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
|
|
||||||
view.hass.http.api_password):
|
|
||||||
authenticated = True
|
|
||||||
|
|
||||||
if view.requires_auth and not authenticated:
|
if view.requires_auth and not authenticated:
|
||||||
|
yield from process_wrong_login(request)
|
||||||
_LOGGER.warning('Login attempt or request with an invalid '
|
_LOGGER.warning('Login attempt or request with an invalid '
|
||||||
'password from %s', remote_addr)
|
'password from %s', remote_addr)
|
||||||
persistent_notification.async_create(
|
persistent_notification.async_create(
|
||||||
view.hass,
|
request.app['hass'],
|
||||||
'Invalid password used from {}'.format(remote_addr),
|
'Invalid password used from {}'.format(remote_addr),
|
||||||
'Login attempt failed', NOTIFICATION_ID_LOGIN)
|
'Login attempt failed', NOTIFICATION_ID_LOGIN)
|
||||||
raise HTTPUnauthorized()
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
request.authenticated = authenticated
|
|
||||||
|
|
||||||
_LOGGER.info('Serving %s to %s (auth: %s)',
|
_LOGGER.info('Serving %s to %s (auth: %s)',
|
||||||
request.path, remote_addr, authenticated)
|
request.path, remote_addr, authenticated)
|
||||||
|
|
||||||
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
|
|
||||||
"Handler should be a coroutine or a callback."
|
|
||||||
|
|
||||||
result = handler(request, **request.match_info)
|
result = handler(request, **request.match_info)
|
||||||
|
|
||||||
if asyncio.iscoroutine(result):
|
if asyncio.iscoroutine(result):
|
66
homeassistant/components/http/auth.py
Normal file
66
homeassistant/components/http/auth.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
"""Authentication for HTTP component."""
|
||||||
|
import asyncio
|
||||||
|
import hmac
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||||
|
from .util import get_real_ip
|
||||||
|
from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
|
||||||
|
|
||||||
|
DATA_API_PASSWORD = 'api_password'
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def auth_middleware(app, handler):
|
||||||
|
"""Authentication middleware."""
|
||||||
|
# If no password set, just always set authenticated=True
|
||||||
|
if app['hass'].http.api_password is None:
|
||||||
|
@asyncio.coroutine
|
||||||
|
def no_auth_middleware_handler(request):
|
||||||
|
"""Auth middleware to approve all requests."""
|
||||||
|
request[KEY_AUTHENTICATED] = True
|
||||||
|
return handler(request)
|
||||||
|
|
||||||
|
return no_auth_middleware_handler
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def auth_middleware_handler(request):
|
||||||
|
"""Auth middleware to check authentication."""
|
||||||
|
# Auth code verbose on purpose
|
||||||
|
authenticated = False
|
||||||
|
|
||||||
|
if (HTTP_HEADER_HA_AUTH in request.headers and
|
||||||
|
validate_password(request,
|
||||||
|
request.headers[HTTP_HEADER_HA_AUTH])):
|
||||||
|
# A valid auth header has been set
|
||||||
|
authenticated = True
|
||||||
|
|
||||||
|
elif (DATA_API_PASSWORD in request.GET and
|
||||||
|
validate_password(request, request.GET[DATA_API_PASSWORD])):
|
||||||
|
authenticated = True
|
||||||
|
|
||||||
|
elif is_trusted_ip(request):
|
||||||
|
authenticated = True
|
||||||
|
|
||||||
|
request[KEY_AUTHENTICATED] = authenticated
|
||||||
|
|
||||||
|
return handler(request)
|
||||||
|
|
||||||
|
return auth_middleware_handler
|
||||||
|
|
||||||
|
|
||||||
|
def is_trusted_ip(request):
|
||||||
|
"""Test if request is from a trusted ip."""
|
||||||
|
ip_addr = get_real_ip(request)
|
||||||
|
|
||||||
|
return ip_addr and any(
|
||||||
|
ip_addr in trusted_network for trusted_network
|
||||||
|
in request.app[KEY_TRUSTED_NETWORKS])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(request, api_password):
|
||||||
|
"""Test if password is valid."""
|
||||||
|
return hmac.compare_digest(api_password,
|
||||||
|
request.app['hass'].http.api_password)
|
132
homeassistant/components/http/ban.py
Normal file
132
homeassistant/components/http/ban.py
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
"""Ban logic for HTTP component."""
|
||||||
|
import asyncio
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from ipaddress import ip_address
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp.web_exceptions import HTTPForbidden
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import persistent_notification
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util.yaml import dump
|
||||||
|
from .const import (
|
||||||
|
KEY_BANS_ENABLED, KEY_BANNED_IPS, KEY_LOGIN_THRESHOLD,
|
||||||
|
KEY_FAILED_LOGIN_ATTEMPTS)
|
||||||
|
from .util import get_real_ip
|
||||||
|
|
||||||
|
NOTIFICATION_ID_BAN = 'ip-ban'
|
||||||
|
|
||||||
|
IP_BANS_FILE = 'ip_bans.yaml'
|
||||||
|
ATTR_BANNED_AT = "banned_at"
|
||||||
|
|
||||||
|
SCHEMA_IP_BAN_ENTRY = vol.Schema({
|
||||||
|
vol.Optional('banned_at'): vol.Any(None, cv.datetime)
|
||||||
|
})
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def ban_middleware(app, handler):
|
||||||
|
"""IP Ban middleware."""
|
||||||
|
if not app[KEY_BANS_ENABLED]:
|
||||||
|
return handler
|
||||||
|
|
||||||
|
if KEY_BANNED_IPS not in app:
|
||||||
|
hass = app['hass']
|
||||||
|
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
|
||||||
|
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def ban_middleware_handler(request):
|
||||||
|
"""Verify if IP is not banned."""
|
||||||
|
ip_address_ = get_real_ip(request)
|
||||||
|
|
||||||
|
is_banned = any(ip_ban.ip_address == ip_address_
|
||||||
|
for ip_ban in request.app[KEY_BANNED_IPS])
|
||||||
|
|
||||||
|
if is_banned:
|
||||||
|
raise HTTPForbidden()
|
||||||
|
|
||||||
|
return handler(request)
|
||||||
|
|
||||||
|
return ban_middleware_handler
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def process_wrong_login(request):
|
||||||
|
"""Process a wrong login attempt."""
|
||||||
|
if (not request.app[KEY_BANS_ENABLED] or
|
||||||
|
request.app[KEY_LOGIN_THRESHOLD] < 1):
|
||||||
|
return
|
||||||
|
|
||||||
|
if KEY_FAILED_LOGIN_ATTEMPTS not in request.app:
|
||||||
|
request.app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
|
||||||
|
|
||||||
|
remote_addr = get_real_ip(request)
|
||||||
|
|
||||||
|
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1
|
||||||
|
|
||||||
|
if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >
|
||||||
|
request.app[KEY_LOGIN_THRESHOLD]):
|
||||||
|
new_ban = IpBan(remote_addr)
|
||||||
|
request.app[KEY_BANNED_IPS].append(new_ban)
|
||||||
|
|
||||||
|
hass = request.app['hass']
|
||||||
|
yield from hass.loop.run_in_executor(
|
||||||
|
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
|
||||||
|
new_ban)
|
||||||
|
|
||||||
|
_LOGGER.warning('Banned IP %s for too many login attempts',
|
||||||
|
remote_addr)
|
||||||
|
|
||||||
|
persistent_notification.async_create(
|
||||||
|
hass,
|
||||||
|
'Too many login attempts from {}'.format(remote_addr),
|
||||||
|
'Banning IP address', NOTIFICATION_ID_BAN)
|
||||||
|
|
||||||
|
|
||||||
|
class IpBan(object):
|
||||||
|
"""Represents banned IP address."""
|
||||||
|
|
||||||
|
def __init__(self, ip_ban: str, banned_at: datetime=None) -> None:
|
||||||
|
"""Initializing Ip Ban object."""
|
||||||
|
self.ip_address = ip_address(ip_ban)
|
||||||
|
self.banned_at = banned_at or datetime.utcnow()
|
||||||
|
|
||||||
|
|
||||||
|
def load_ip_bans_config(path: str):
|
||||||
|
"""Loading list of banned IPs from config file."""
|
||||||
|
ip_list = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
list_ = load_yaml_config_file(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return []
|
||||||
|
except HomeAssistantError as err:
|
||||||
|
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||||
|
return []
|
||||||
|
|
||||||
|
for ip_ban, ip_info in list_.items():
|
||||||
|
try:
|
||||||
|
ip_info = SCHEMA_IP_BAN_ENTRY(ip_info)
|
||||||
|
ip_list.append(IpBan(ip_ban, ip_info['banned_at']))
|
||||||
|
except vol.Invalid as err:
|
||||||
|
_LOGGER.error('Failed to load IP ban %s: %s', ip_info, err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return ip_list
|
||||||
|
|
||||||
|
|
||||||
|
def update_ip_bans_config(path: str, ip_ban: IpBan):
|
||||||
|
"""Update config file with new banned IP address."""
|
||||||
|
with open(path, 'a') as out:
|
||||||
|
ip_ = {str(ip_ban.ip_address): {
|
||||||
|
ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
}}
|
||||||
|
out.write('\n')
|
||||||
|
out.write(dump(ip_))
|
12
homeassistant/components/http/const.py
Normal file
12
homeassistant/components/http/const.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""HTTP specific constants."""
|
||||||
|
KEY_AUTHENTICATED = 'ha_authenticated'
|
||||||
|
KEY_USE_X_FORWARDED_FOR = 'ha_use_x_forwarded_for'
|
||||||
|
KEY_TRUSTED_NETWORKS = 'ha_trusted_networks'
|
||||||
|
KEY_REAL_IP = 'ha_real_ip'
|
||||||
|
KEY_BANS_ENABLED = 'ha_bans_enabled'
|
||||||
|
KEY_BANNED_IPS = 'ha_banned_ips'
|
||||||
|
KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
|
||||||
|
KEY_LOGIN_THRESHOLD = 'ha_login_treshold'
|
||||||
|
KEY_DEVELOPMENT = 'ha_development'
|
||||||
|
|
||||||
|
HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'
|
93
homeassistant/components/http/static.py
Normal file
93
homeassistant/components/http/static.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
"""Static file handling for HTTP component."""
|
||||||
|
import asyncio
|
||||||
|
import mimetypes
|
||||||
|
import re
|
||||||
|
|
||||||
|
from aiohttp import hdrs
|
||||||
|
from aiohttp.file_sender import FileSender
|
||||||
|
from aiohttp.web_urldispatcher import StaticResource
|
||||||
|
from aiohttp.web_exceptions import HTTPNotModified
|
||||||
|
|
||||||
|
from .const import KEY_DEVELOPMENT
|
||||||
|
|
||||||
|
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
class GzipFileSender(FileSender):
|
||||||
|
"""FileSender class capable of sending gzip version if available."""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def send(self, request, filepath):
|
||||||
|
"""Send filepath to client using request."""
|
||||||
|
gzip = False
|
||||||
|
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
|
||||||
|
gzip_path = filepath.with_name(filepath.name + '.gz')
|
||||||
|
|
||||||
|
if gzip_path.is_file():
|
||||||
|
filepath = gzip_path
|
||||||
|
gzip = True
|
||||||
|
|
||||||
|
st = filepath.stat()
|
||||||
|
|
||||||
|
modsince = request.if_modified_since
|
||||||
|
if modsince is not None and st.st_mtime <= modsince.timestamp():
|
||||||
|
raise HTTPNotModified()
|
||||||
|
|
||||||
|
ct, encoding = mimetypes.guess_type(str(filepath))
|
||||||
|
if not ct:
|
||||||
|
ct = 'application/octet-stream'
|
||||||
|
|
||||||
|
resp = self._response_factory()
|
||||||
|
resp.content_type = ct
|
||||||
|
if encoding:
|
||||||
|
resp.headers[hdrs.CONTENT_ENCODING] = encoding
|
||||||
|
if gzip:
|
||||||
|
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
|
||||||
|
resp.last_modified = st.st_mtime
|
||||||
|
|
||||||
|
# CACHE HACK
|
||||||
|
if not request.app[KEY_DEVELOPMENT]:
|
||||||
|
cache_time = 31 * 86400 # = 1 month
|
||||||
|
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
|
||||||
|
cache_time)
|
||||||
|
|
||||||
|
file_size = st.st_size
|
||||||
|
|
||||||
|
resp.content_length = file_size
|
||||||
|
with filepath.open('rb') as f:
|
||||||
|
yield from self._sendfile(request, resp, f, file_size)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
GZIP_FILE_SENDER = GzipFileSender()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def staticresource_middleware(app, handler):
|
||||||
|
"""Enhance StaticResourceHandler middleware.
|
||||||
|
|
||||||
|
Adds gzip encoding and fingerprinting matching.
|
||||||
|
"""
|
||||||
|
inst = getattr(handler, '__self__', None)
|
||||||
|
if not isinstance(inst, StaticResource):
|
||||||
|
return handler
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
inst._file_sender = GZIP_FILE_SENDER
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def static_middleware_handler(request):
|
||||||
|
"""Strip out fingerprints from resource names."""
|
||||||
|
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
||||||
|
|
||||||
|
if fingerprinted:
|
||||||
|
request.match_info['filename'] = \
|
||||||
|
'{}.{}'.format(*fingerprinted.groups())
|
||||||
|
|
||||||
|
resp = yield from handler(request)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
return static_middleware_handler
|
25
homeassistant/components/http/util.py
Normal file
25
homeassistant/components/http/util.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""HTTP utilities."""
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
KEY_REAL_IP, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR)
|
||||||
|
|
||||||
|
|
||||||
|
def get_real_ip(request):
|
||||||
|
"""Get IP address of client."""
|
||||||
|
if KEY_REAL_IP in request:
|
||||||
|
return request[KEY_REAL_IP]
|
||||||
|
|
||||||
|
if (request.app[KEY_USE_X_FORWARDED_FOR] and
|
||||||
|
HTTP_HEADER_X_FORWARDED_FOR in request.headers):
|
||||||
|
request[KEY_REAL_IP] = ip_address(
|
||||||
|
request.headers.get(HTTP_HEADER_X_FORWARDED_FOR).split(',')[0])
|
||||||
|
else:
|
||||||
|
peername = request.transport.get_extra_info('peername')
|
||||||
|
|
||||||
|
if peername:
|
||||||
|
request[KEY_REAL_IP] = ip_address(peername[0])
|
||||||
|
else:
|
||||||
|
request[KEY_REAL_IP] = None
|
||||||
|
|
||||||
|
return request[KEY_REAL_IP]
|
|
@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_DB_NAME = 'database'
|
CONF_DB_NAME = 'database'
|
||||||
CONF_TAGS = 'tags'
|
CONF_TAGS = 'tags'
|
||||||
|
CONF_DEFAULT_MEASUREMENT = 'default_measurement'
|
||||||
|
|
||||||
DEFAULT_DATABASE = 'home_assistant'
|
DEFAULT_DATABASE = 'home_assistant'
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
|
@ -40,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
|
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||||
|
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
|
||||||
vol.Optional(CONF_TAGS, default={}):
|
vol.Optional(CONF_TAGS, default={}):
|
||||||
vol.Schema({cv.string: cv.string}),
|
vol.Schema({cv.string: cv.string}),
|
||||||
vol.Optional(CONF_WHITELIST, default=[]):
|
vol.Optional(CONF_WHITELIST, default=[]):
|
||||||
|
@ -65,6 +67,7 @@ def setup(hass, config):
|
||||||
blacklist = conf.get(CONF_BLACKLIST)
|
blacklist = conf.get(CONF_BLACKLIST)
|
||||||
whitelist = conf.get(CONF_WHITELIST)
|
whitelist = conf.get(CONF_WHITELIST)
|
||||||
tags = conf.get(CONF_TAGS)
|
tags = conf.get(CONF_TAGS)
|
||||||
|
default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
influx = InfluxDBClient(
|
influx = InfluxDBClient(
|
||||||
|
@ -96,6 +99,9 @@ def setup(hass, config):
|
||||||
|
|
||||||
measurement = state.attributes.get('unit_of_measurement')
|
measurement = state.attributes.get('unit_of_measurement')
|
||||||
if measurement in (None, ''):
|
if measurement in (None, ''):
|
||||||
|
if default_measurement:
|
||||||
|
measurement = default_measurement
|
||||||
|
else:
|
||||||
measurement = state.entity_id
|
measurement = state.entity_id
|
||||||
|
|
||||||
json_body = [
|
json_body = [
|
||||||
|
@ -114,7 +120,11 @@ def setup(hass, config):
|
||||||
|
|
||||||
for key, value in state.attributes.items():
|
for key, value in state.attributes.items():
|
||||||
if key != 'unit_of_measurement':
|
if key != 'unit_of_measurement':
|
||||||
|
if isinstance(value, (str, float, bool)):
|
||||||
json_body[0]['fields'][key] = value
|
json_body[0]['fields'][key] = value
|
||||||
|
elif isinstance(value, int):
|
||||||
|
# Prevent column data errors in influxDB.
|
||||||
|
json_body[0]['fields'][key] = float(value)
|
||||||
|
|
||||||
json_body[0]['tags'].update(tags)
|
json_body[0]['tags'].update(tags)
|
||||||
|
|
||||||
|
|
|
@ -250,11 +250,10 @@ def setup(hass, config):
|
||||||
|
|
||||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||||
|
|
||||||
hass.http.register_view(iOSIdentifyDeviceView(hass))
|
hass.http.register_view(iOSIdentifyDeviceView)
|
||||||
|
|
||||||
app_config = config.get(DOMAIN, {})
|
app_config = config.get(DOMAIN, {})
|
||||||
hass.http.register_view(iOSPushConfigView(hass,
|
hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {})))
|
||||||
app_config.get(CONF_PUSH, {})))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -266,9 +265,8 @@ class iOSPushConfigView(HomeAssistantView):
|
||||||
url = "/api/ios/push"
|
url = "/api/ios/push"
|
||||||
name = "api:ios:push"
|
name = "api:ios:push"
|
||||||
|
|
||||||
def __init__(self, hass, push_config):
|
def __init__(self, push_config):
|
||||||
"""Init the view."""
|
"""Init the view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.push_config = push_config
|
self.push_config = push_config
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -283,10 +281,6 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||||
url = "/api/ios/identify"
|
url = "/api/ios/identify"
|
||||||
name = "api:ios:identify"
|
name = "api:ios:identify"
|
||||||
|
|
||||||
def __init__(self, hass):
|
|
||||||
"""Init the view."""
|
|
||||||
super().__init__(hass)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Handle the POST request for device identification."""
|
"""Handle the POST request for device identification."""
|
||||||
|
|
|
@ -4,6 +4,7 @@ Provides functionality to interact with lights.
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/light/
|
https://home-assistant.io/components/light/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
|
@ -64,6 +65,9 @@ ATTR_FLASH = "flash"
|
||||||
FLASH_SHORT = "short"
|
FLASH_SHORT = "short"
|
||||||
FLASH_LONG = "long"
|
FLASH_LONG = "long"
|
||||||
|
|
||||||
|
# List of possible effects
|
||||||
|
ATTR_EFFECT_LIST = "effect_list"
|
||||||
|
|
||||||
# Apply an effect to the light, can be EFFECT_COLORLOOP.
|
# Apply an effect to the light, can be EFFECT_COLORLOOP.
|
||||||
ATTR_EFFECT = "effect"
|
ATTR_EFFECT = "effect"
|
||||||
EFFECT_COLORLOOP = "colorloop"
|
EFFECT_COLORLOOP = "colorloop"
|
||||||
|
@ -78,6 +82,8 @@ PROP_TO_ATTR = {
|
||||||
'rgb_color': ATTR_RGB_COLOR,
|
'rgb_color': ATTR_RGB_COLOR,
|
||||||
'xy_color': ATTR_XY_COLOR,
|
'xy_color': ATTR_XY_COLOR,
|
||||||
'white_value': ATTR_WHITE_VALUE,
|
'white_value': ATTR_WHITE_VALUE,
|
||||||
|
'effect_list': ATTR_EFFECT_LIST,
|
||||||
|
'effect': ATTR_EFFECT,
|
||||||
'supported_features': ATTR_SUPPORTED_FEATURES,
|
'supported_features': ATTR_SUPPORTED_FEATURES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,19 +93,20 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
|
||||||
|
|
||||||
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
ATTR_PROFILE: str,
|
ATTR_PROFILE: cv.string,
|
||||||
ATTR_TRANSITION: VALID_TRANSITION,
|
ATTR_TRANSITION: VALID_TRANSITION,
|
||||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||||
ATTR_COLOR_NAME: str,
|
ATTR_COLOR_NAME: cv.string,
|
||||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=color_util.HASS_COLOR_MIN,
|
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int),
|
||||||
|
vol.Range(min=color_util.HASS_COLOR_MIN,
|
||||||
max=color_util.HASS_COLOR_MAX)),
|
max=color_util.HASS_COLOR_MAX)),
|
||||||
ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)),
|
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||||
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||||
ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]),
|
ATTR_EFFECT: cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
LIGHT_TURN_OFF_SCHEMA = vol.Schema({
|
LIGHT_TURN_OFF_SCHEMA = vol.Schema({
|
||||||
|
@ -158,7 +165,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||||
] if value is not None
|
] if value is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data)
|
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||||
|
|
||||||
|
|
||||||
def turn_off(hass, entity_id=None, transition=None):
|
def turn_off(hass, entity_id=None, transition=None):
|
||||||
|
@ -177,8 +184,8 @@ def async_turn_off(hass, entity_id=None, transition=None):
|
||||||
] if value is not None
|
] if value is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF,
|
hass.async_add_job(hass.services.async_call(
|
||||||
data)
|
DOMAIN, SERVICE_TURN_OFF, data))
|
||||||
|
|
||||||
|
|
||||||
def toggle(hass, entity_id=None, transition=None):
|
def toggle(hass, entity_id=None, transition=None):
|
||||||
|
@ -193,13 +200,83 @@ def toggle(hass, entity_id=None, transition=None):
|
||||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
"""Expose light control via statemachine and services."""
|
"""Expose light control via statemachine and services."""
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
|
||||||
component.setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
# Load built-in profiles and custom profiles
|
# load profiles from files
|
||||||
|
profiles = yield from hass.loop.run_in_executor(
|
||||||
|
None, _load_profile_data, hass)
|
||||||
|
|
||||||
|
if profiles is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_handle_light_service(service):
|
||||||
|
"""Hande a turn light on or off service call."""
|
||||||
|
# Get the validated data
|
||||||
|
params = service.data.copy()
|
||||||
|
|
||||||
|
# Convert the entity ids to valid light ids
|
||||||
|
target_lights = component.async_extract_from_service(service)
|
||||||
|
params.pop(ATTR_ENTITY_ID, None)
|
||||||
|
|
||||||
|
# Processing extra data for turn light on request.
|
||||||
|
profile = profiles.get(params.pop(ATTR_PROFILE, None))
|
||||||
|
|
||||||
|
if profile:
|
||||||
|
params.setdefault(ATTR_XY_COLOR, profile[:2])
|
||||||
|
params.setdefault(ATTR_BRIGHTNESS, profile[2])
|
||||||
|
|
||||||
|
color_name = params.pop(ATTR_COLOR_NAME, None)
|
||||||
|
|
||||||
|
if color_name is not None:
|
||||||
|
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||||
|
|
||||||
|
update_tasks = []
|
||||||
|
for light in target_lights:
|
||||||
|
if service.service == SERVICE_TURN_ON:
|
||||||
|
yield from light.async_turn_on(**params)
|
||||||
|
elif service.service == SERVICE_TURN_OFF:
|
||||||
|
yield from light.async_turn_off(**params)
|
||||||
|
else:
|
||||||
|
yield from light.async_toggle(**params)
|
||||||
|
|
||||||
|
if light.should_poll:
|
||||||
|
update_coro = light.async_update_ha_state(True)
|
||||||
|
if hasattr(light, 'async_update'):
|
||||||
|
update_tasks.append(hass.loop.create_task(update_coro))
|
||||||
|
else:
|
||||||
|
yield from update_coro
|
||||||
|
|
||||||
|
if update_tasks:
|
||||||
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
|
# Listen for light on and light off service calls.
|
||||||
|
descriptions = yield from hass.loop.run_in_executor(
|
||||||
|
None, load_yaml_config_file, os.path.join(
|
||||||
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_TURN_ON, async_handle_light_service,
|
||||||
|
descriptions.get(SERVICE_TURN_ON), schema=LIGHT_TURN_ON_SCHEMA)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_TURN_OFF, async_handle_light_service,
|
||||||
|
descriptions.get(SERVICE_TURN_OFF), schema=LIGHT_TURN_OFF_SCHEMA)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_TOGGLE, async_handle_light_service,
|
||||||
|
descriptions.get(SERVICE_TOGGLE), schema=LIGHT_TOGGLE_SCHEMA)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _load_profile_data(hass):
|
||||||
|
"""Load built-in profiles and custom profiles."""
|
||||||
profile_paths = [os.path.join(os.path.dirname(__file__),
|
profile_paths = [os.path.join(os.path.dirname(__file__),
|
||||||
LIGHT_PROFILES_FILE),
|
LIGHT_PROFILES_FILE),
|
||||||
hass.config.path(LIGHT_PROFILES_FILE)]
|
hass.config.path(LIGHT_PROFILES_FILE)]
|
||||||
|
@ -221,67 +298,8 @@ def setup(hass, config):
|
||||||
except vol.MultipleInvalid as ex:
|
except vol.MultipleInvalid as ex:
|
||||||
_LOGGER.error("Error parsing light profile from %s: %s",
|
_LOGGER.error("Error parsing light profile from %s: %s",
|
||||||
profile_path, ex)
|
profile_path, ex)
|
||||||
return False
|
return None
|
||||||
|
return profiles
|
||||||
def handle_light_service(service):
|
|
||||||
"""Hande a turn light on or off service call."""
|
|
||||||
# Get the validated data
|
|
||||||
params = service.data.copy()
|
|
||||||
|
|
||||||
# Convert the entity ids to valid light ids
|
|
||||||
target_lights = component.extract_from_service(service)
|
|
||||||
params.pop(ATTR_ENTITY_ID, None)
|
|
||||||
|
|
||||||
service_fun = None
|
|
||||||
if service.service == SERVICE_TURN_OFF:
|
|
||||||
service_fun = 'turn_off'
|
|
||||||
elif service.service == SERVICE_TOGGLE:
|
|
||||||
service_fun = 'toggle'
|
|
||||||
|
|
||||||
if service_fun:
|
|
||||||
for light in target_lights:
|
|
||||||
getattr(light, service_fun)(**params)
|
|
||||||
|
|
||||||
for light in target_lights:
|
|
||||||
if light.should_poll:
|
|
||||||
light.update_ha_state(True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Processing extra data for turn light on request.
|
|
||||||
profile = profiles.get(params.pop(ATTR_PROFILE, None))
|
|
||||||
|
|
||||||
if profile:
|
|
||||||
params.setdefault(ATTR_XY_COLOR, profile[:2])
|
|
||||||
params.setdefault(ATTR_BRIGHTNESS, profile[2])
|
|
||||||
|
|
||||||
color_name = params.pop(ATTR_COLOR_NAME, None)
|
|
||||||
|
|
||||||
if color_name is not None:
|
|
||||||
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
|
||||||
|
|
||||||
for light in target_lights:
|
|
||||||
light.turn_on(**params)
|
|
||||||
|
|
||||||
for light in target_lights:
|
|
||||||
if light.should_poll:
|
|
||||||
light.update_ha_state(True)
|
|
||||||
|
|
||||||
# Listen for light on and light off service calls.
|
|
||||||
descriptions = load_yaml_config_file(
|
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
|
|
||||||
descriptions.get(SERVICE_TURN_ON),
|
|
||||||
schema=LIGHT_TURN_ON_SCHEMA)
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
|
||||||
descriptions.get(SERVICE_TURN_OFF),
|
|
||||||
schema=LIGHT_TURN_OFF_SCHEMA)
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service,
|
|
||||||
descriptions.get(SERVICE_TOGGLE),
|
|
||||||
schema=LIGHT_TOGGLE_SCHEMA)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Light(ToggleEntity):
|
class Light(ToggleEntity):
|
||||||
|
@ -314,6 +332,16 @@ class Light(ToggleEntity):
|
||||||
"""Return the white value of this light between 0..255."""
|
"""Return the white value of this light between 0..255."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self):
|
||||||
|
"""Return the list of supported effects."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self):
|
||||||
|
"""Return the current effect."""
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return optional state attributes."""
|
"""Return optional state attributes."""
|
||||||
|
|
|
@ -7,25 +7,29 @@ https://home-assistant.io/components/demo/
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_WHITE_VALUE,
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT,
|
||||||
ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS,
|
||||||
SUPPORT_WHITE_VALUE, Light)
|
SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE,
|
||||||
|
Light)
|
||||||
|
|
||||||
LIGHT_COLORS = [
|
LIGHT_COLORS = [
|
||||||
[237, 224, 33],
|
[237, 224, 33],
|
||||||
[255, 63, 111],
|
[255, 63, 111],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LIGHT_EFFECT_LIST = ['rainbow', 'none']
|
||||||
|
|
||||||
LIGHT_TEMPS = [240, 380]
|
LIGHT_TEMPS = [240, 380]
|
||||||
|
|
||||||
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
|
||||||
SUPPORT_WHITE_VALUE)
|
SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
"""Setup the demo light platform."""
|
"""Setup the demo light platform."""
|
||||||
add_devices_callback([
|
add_devices_callback([
|
||||||
DemoLight("Bed Light", False),
|
DemoLight("Bed Light", False, effect_list=LIGHT_EFFECT_LIST,
|
||||||
|
effect=LIGHT_EFFECT_LIST[0]),
|
||||||
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
|
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
|
||||||
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
|
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
|
||||||
])
|
])
|
||||||
|
@ -36,7 +40,7 @@ class DemoLight(Light):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name, state, rgb=None, ct=None, brightness=180,
|
self, name, state, rgb=None, ct=None, brightness=180,
|
||||||
xy_color=(.5, .5), white=200):
|
xy_color=(.5, .5), white=200, effect_list=None, effect=None):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = state
|
self._state = state
|
||||||
|
@ -45,6 +49,8 @@ class DemoLight(Light):
|
||||||
self._brightness = brightness
|
self._brightness = brightness
|
||||||
self._xy_color = xy_color
|
self._xy_color = xy_color
|
||||||
self._white = white
|
self._white = white
|
||||||
|
self._effect_list = effect_list
|
||||||
|
self._effect = effect
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
@ -81,6 +87,16 @@ class DemoLight(Light):
|
||||||
"""Return the white value of this light between 0..255."""
|
"""Return the white value of this light between 0..255."""
|
||||||
return self._white
|
return self._white
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self):
|
||||||
|
"""Return the list of supported effects."""
|
||||||
|
return self._effect_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self):
|
||||||
|
"""Return the current effect."""
|
||||||
|
return self._effect
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if light is on."""
|
"""Return true if light is on."""
|
||||||
|
@ -110,9 +126,12 @@ class DemoLight(Light):
|
||||||
if ATTR_WHITE_VALUE in kwargs:
|
if ATTR_WHITE_VALUE in kwargs:
|
||||||
self._white = kwargs[ATTR_WHITE_VALUE]
|
self._white = kwargs[ATTR_WHITE_VALUE]
|
||||||
|
|
||||||
self.update_ha_state()
|
if ATTR_EFFECT in kwargs:
|
||||||
|
self._effect = kwargs[ATTR_EFFECT]
|
||||||
|
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
self._state = False
|
self._state = False
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
@ -7,8 +7,9 @@ https://home-assistant.io/components/light.homematic/
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
||||||
SUPPORT_BRIGHTNESS, Light)
|
SUPPORT_BRIGHTNESS, Light)
|
||||||
|
from homeassistant.components.homematic import HMDevice
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
import homeassistant.components.homematic as homematic
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -22,14 +23,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
homematic = get_component("homematic")
|
||||||
return homematic.setup_hmdevice_discovery_helper(
|
return homematic.setup_hmdevice_discovery_helper(
|
||||||
|
hass,
|
||||||
HMLight,
|
HMLight,
|
||||||
discovery_info,
|
discovery_info,
|
||||||
add_devices
|
add_devices
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HMLight(homematic.HMDevice, Light):
|
class HMLight(HMDevice, Light):
|
||||||
"""Representation of a Homematic light."""
|
"""Representation of a Homematic light."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -61,10 +61,10 @@ class ISYLightDevice(isy.ISYDevice, Light):
|
||||||
|
|
||||||
def turn_off(self, **kwargs) -> None:
|
def turn_off(self, **kwargs) -> None:
|
||||||
"""Send the turn off command to the ISY994 light device."""
|
"""Send the turn off command to the ISY994 light device."""
|
||||||
if not self._node.fastoff():
|
if not self._node.off():
|
||||||
_LOGGER.debug('Unable to turn on light.')
|
_LOGGER.debug('Unable to turn on light.')
|
||||||
|
|
||||||
def turn_on(self, brightness=100, **kwargs) -> None:
|
def turn_on(self, brightness=None, **kwargs) -> None:
|
||||||
"""Send the turn on command to the ISY994 light device."""
|
"""Send the turn on command to the ISY994 light device."""
|
||||||
if not self._node.on(val=brightness):
|
if not self._node.on(val=brightness):
|
||||||
_LOGGER.debug('Unable to turn on light.')
|
_LOGGER.debug('Unable to turn on light.')
|
||||||
|
|
|
@ -140,7 +140,7 @@ def state(new_state):
|
||||||
# Update state.
|
# Update state.
|
||||||
self._is_on = new_state
|
self._is_on = new_state
|
||||||
self.group.enqueue(pipeline)
|
self.group.enqueue(pipeline)
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
|
@ -283,7 +283,7 @@ class MqttLight(Light):
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
if should_update:
|
if should_update:
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
@ -293,4 +293,4 @@ class MqttLight(Light):
|
||||||
if self._optimistic:
|
if self._optimistic:
|
||||||
# Optimistically assume that switch has changed state.
|
# Optimistically assume that switch has changed state.
|
||||||
self._state = False
|
self._state = False
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
@ -216,7 +216,7 @@ class MqttJson(Light):
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
if should_update:
|
if should_update:
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
@ -231,4 +231,4 @@ class MqttJson(Light):
|
||||||
if self._optimistic:
|
if self._optimistic:
|
||||||
# Optimistically assume that the light has changed state.
|
# Optimistically assume that the light has changed state.
|
||||||
self._state = False
|
self._state = False
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
@ -10,8 +10,8 @@ import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA,
|
ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||||
ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH,
|
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH,
|
||||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
||||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
|
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
|
@ -27,6 +27,7 @@ DEPENDENCIES = ['mqtt']
|
||||||
DEFAULT_NAME = 'MQTT Template Light'
|
DEFAULT_NAME = 'MQTT Template Light'
|
||||||
DEFAULT_OPTIMISTIC = False
|
DEFAULT_OPTIMISTIC = False
|
||||||
|
|
||||||
|
CONF_EFFECT_LIST = "effect_list"
|
||||||
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
|
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
|
||||||
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
|
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
|
||||||
CONF_STATE_TEMPLATE = 'state_template'
|
CONF_STATE_TEMPLATE = 'state_template'
|
||||||
|
@ -34,12 +35,14 @@ CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
|
||||||
CONF_RED_TEMPLATE = 'red_template'
|
CONF_RED_TEMPLATE = 'red_template'
|
||||||
CONF_GREEN_TEMPLATE = 'green_template'
|
CONF_GREEN_TEMPLATE = 'green_template'
|
||||||
CONF_BLUE_TEMPLATE = 'blue_template'
|
CONF_BLUE_TEMPLATE = 'blue_template'
|
||||||
|
CONF_EFFECT_TEMPLATE = 'effect_template'
|
||||||
|
|
||||||
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH |
|
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH |
|
||||||
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
|
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
|
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
|
||||||
|
@ -49,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_RED_TEMPLATE): cv.template,
|
vol.Optional(CONF_RED_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
|
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_EFFECT_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||||
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
|
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
|
||||||
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
|
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
|
||||||
|
@ -61,6 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
add_devices([MqttTemplate(
|
add_devices([MqttTemplate(
|
||||||
hass,
|
hass,
|
||||||
config.get(CONF_NAME),
|
config.get(CONF_NAME),
|
||||||
|
config.get(CONF_EFFECT_LIST),
|
||||||
{
|
{
|
||||||
key: config.get(key) for key in (
|
key: config.get(key) for key in (
|
||||||
CONF_STATE_TOPIC,
|
CONF_STATE_TOPIC,
|
||||||
|
@ -75,7 +80,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
CONF_BRIGHTNESS_TEMPLATE,
|
CONF_BRIGHTNESS_TEMPLATE,
|
||||||
CONF_RED_TEMPLATE,
|
CONF_RED_TEMPLATE,
|
||||||
CONF_GREEN_TEMPLATE,
|
CONF_GREEN_TEMPLATE,
|
||||||
CONF_BLUE_TEMPLATE
|
CONF_BLUE_TEMPLATE,
|
||||||
|
CONF_EFFECT_TEMPLATE
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
config.get(CONF_OPTIMISTIC),
|
config.get(CONF_OPTIMISTIC),
|
||||||
|
@ -87,10 +93,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
class MqttTemplate(Light):
|
class MqttTemplate(Light):
|
||||||
"""Representation of a MQTT Template light."""
|
"""Representation of a MQTT Template light."""
|
||||||
|
|
||||||
def __init__(self, hass, name, topics, templates, optimistic, qos, retain):
|
def __init__(self, hass, name, effect_list, topics, templates, optimistic,
|
||||||
|
qos, retain):
|
||||||
"""Initialize MQTT Template light."""
|
"""Initialize MQTT Template light."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._effect_list = effect_list
|
||||||
self._topics = topics
|
self._topics = topics
|
||||||
self._templates = templates
|
self._templates = templates
|
||||||
for tpl in self._templates.values():
|
for tpl in self._templates.values():
|
||||||
|
@ -114,6 +122,7 @@ class MqttTemplate(Light):
|
||||||
self._rgb = [0, 0, 0]
|
self._rgb = [0, 0, 0]
|
||||||
else:
|
else:
|
||||||
self._rgb = None
|
self._rgb = None
|
||||||
|
self._effect = None
|
||||||
|
|
||||||
def state_received(topic, payload, qos):
|
def state_received(topic, payload, qos):
|
||||||
"""A new MQTT message has been received."""
|
"""A new MQTT message has been received."""
|
||||||
|
@ -152,6 +161,17 @@ class MqttTemplate(Light):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_LOGGER.warning('Invalid color value received')
|
_LOGGER.warning('Invalid color value received')
|
||||||
|
|
||||||
|
# read effect
|
||||||
|
if self._templates[CONF_EFFECT_TEMPLATE] is not None:
|
||||||
|
effect = self._templates[CONF_EFFECT_TEMPLATE].\
|
||||||
|
render_with_possible_json_value(payload)
|
||||||
|
|
||||||
|
# validate effect value
|
||||||
|
if effect in self._effect_list:
|
||||||
|
self._effect = effect
|
||||||
|
else:
|
||||||
|
_LOGGER.warning('Unsupported effect value received')
|
||||||
|
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
if self._topics[CONF_STATE_TOPIC] is not None:
|
if self._topics[CONF_STATE_TOPIC] is not None:
|
||||||
|
@ -191,6 +211,16 @@ class MqttTemplate(Light):
|
||||||
"""Return True if unable to access real state of the entity."""
|
"""Return True if unable to access real state of the entity."""
|
||||||
return self._optimistic
|
return self._optimistic
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self):
|
||||||
|
"""Return the list of supported effects."""
|
||||||
|
return self._effect_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self):
|
||||||
|
"""Return the current effect."""
|
||||||
|
return self._effect
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
# state
|
# state
|
||||||
|
@ -214,6 +244,10 @@ class MqttTemplate(Light):
|
||||||
if self._optimistic:
|
if self._optimistic:
|
||||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||||
|
|
||||||
|
# effect
|
||||||
|
if ATTR_EFFECT in kwargs:
|
||||||
|
values['effect'] = kwargs.get(ATTR_EFFECT)
|
||||||
|
|
||||||
# flash
|
# flash
|
||||||
if ATTR_FLASH in kwargs:
|
if ATTR_FLASH in kwargs:
|
||||||
values['flash'] = kwargs.get(ATTR_FLASH)
|
values['flash'] = kwargs.get(ATTR_FLASH)
|
||||||
|
@ -229,7 +263,7 @@ class MqttTemplate(Light):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._optimistic:
|
if self._optimistic:
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
|
@ -249,4 +283,4 @@ class MqttTemplate(Light):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._optimistic:
|
if self._optimistic:
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
@ -115,7 +115,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
||||||
# optimistically assume that light has changed state
|
# optimistically assume that light has changed state
|
||||||
self._state = True
|
self._state = True
|
||||||
self._values[set_req.V_LIGHT] = STATE_ON
|
self._values[set_req.V_LIGHT] = STATE_ON
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _turn_on_dimmer(self, **kwargs):
|
def _turn_on_dimmer(self, **kwargs):
|
||||||
"""Turn on dimmer child device."""
|
"""Turn on dimmer child device."""
|
||||||
|
@ -135,7 +135,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
||||||
# optimistically assume that light has changed state
|
# optimistically assume that light has changed state
|
||||||
self._brightness = brightness
|
self._brightness = brightness
|
||||||
self._values[set_req.V_DIMMER] = percent
|
self._values[set_req.V_DIMMER] = percent
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _turn_on_rgb_and_w(self, hex_template, **kwargs):
|
def _turn_on_rgb_and_w(self, hex_template, **kwargs):
|
||||||
"""Turn on RGB or RGBW child device."""
|
"""Turn on RGB or RGBW child device."""
|
||||||
|
@ -165,7 +165,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
||||||
self._white = white
|
self._white = white
|
||||||
if hex_color:
|
if hex_color:
|
||||||
self._values[self.value_type] = hex_color
|
self._values[self.value_type] = hex_color
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _turn_off_light(self, value_type=None, value=None):
|
def _turn_off_light(self, value_type=None, value=None):
|
||||||
"""Turn off light child device."""
|
"""Turn off light child device."""
|
||||||
|
@ -211,7 +211,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
||||||
self._state = False
|
self._state = False
|
||||||
self._values[value_type] = (
|
self._values[value_type] = (
|
||||||
STATE_OFF if set_req.V_LIGHT in self._values else value)
|
STATE_OFF if set_req.V_LIGHT in self._values else value)
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _update_light(self):
|
def _update_light(self):
|
||||||
"""Update the controller with values from light child."""
|
"""Update the controller with values from light child."""
|
||||||
|
|
|
@ -19,7 +19,8 @@ from homeassistant.components.light import (
|
||||||
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
|
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['lightify==1.0.3']
|
REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/'
|
||||||
|
'd6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -92,37 +93,43 @@ class OsramLightifyLight(Light):
|
||||||
self._light = light
|
self._light = light
|
||||||
self._light_id = light_id
|
self._light_id = light_id
|
||||||
self.update_lights = update_lights
|
self.update_lights = update_lights
|
||||||
|
self._brightness = 0
|
||||||
|
self._rgb = (0, 0, 0)
|
||||||
|
self._name = ""
|
||||||
|
self._temperature = TEMP_MIN
|
||||||
|
self._state = False
|
||||||
|
self.update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device if any."""
|
"""Return the name of the device if any."""
|
||||||
return self._light.name()
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rgb_color(self):
|
def rgb_color(self):
|
||||||
"""Last RGB color value set."""
|
"""Last RGB color value set."""
|
||||||
return self._light.rgb()
|
_LOGGER.debug("rgb_color light state for light: %s is: %s %s %s ",
|
||||||
|
self._name, self._rgb[0], self._rgb[1], self._rgb[2])
|
||||||
|
return self._rgb
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_temp(self):
|
def color_temp(self):
|
||||||
"""Return the color temperature."""
|
"""Return the color temperature."""
|
||||||
o_temp = self._light.temp()
|
return self._temperature
|
||||||
temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) *
|
|
||||||
(o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
|
|
||||||
return temperature
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Brightness of this light between 0..255."""
|
"""Brightness of this light between 0..255."""
|
||||||
return int(self._light.lum() * 2.55)
|
_LOGGER.debug("brightness for light %s is: %s",
|
||||||
|
self._name, self._brightness)
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Update Status to True if device is on."""
|
"""Update Status to True if device is on."""
|
||||||
self.update_lights()
|
|
||||||
_LOGGER.debug("is_on light state for light: %s is: %s",
|
_LOGGER.debug("is_on light state for light: %s is: %s",
|
||||||
self._light.name(), self._light.on())
|
self._name, self._state)
|
||||||
return self._light.on()
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
|
@ -131,47 +138,86 @@ class OsramLightifyLight(Light):
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
brightness = 100
|
_LOGGER.debug("turn_on Attempting to turn on light: %s ",
|
||||||
if self.brightness:
|
self._name)
|
||||||
brightness = int(self.brightness / 2.55)
|
|
||||||
|
self._light.set_onoff(1)
|
||||||
|
self._state = self._light.on()
|
||||||
|
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
fade = kwargs[ATTR_TRANSITION] * 10
|
transition = kwargs[ATTR_TRANSITION] * 10
|
||||||
|
_LOGGER.debug("turn_on requested transition time for light:"
|
||||||
|
" %s is: %s ",
|
||||||
|
self._name, transition)
|
||||||
else:
|
else:
|
||||||
fade = 0
|
transition = 0
|
||||||
|
_LOGGER.debug("turn_on requested transition time for light:"
|
||||||
|
" %s is: %s ",
|
||||||
|
self._name, transition)
|
||||||
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
red, green, blue = kwargs[ATTR_RGB_COLOR]
|
red, green, blue = kwargs[ATTR_RGB_COLOR]
|
||||||
self._light.set_rgb(red, green, blue, fade)
|
_LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:"
|
||||||
|
" %s is: %s %s %s ",
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
self._name, red, green, blue)
|
||||||
brightness = int(kwargs[ATTR_BRIGHTNESS] / 2.55)
|
self._light.set_rgb(red, green, blue, transition)
|
||||||
|
|
||||||
if ATTR_COLOR_TEMP in kwargs:
|
if ATTR_COLOR_TEMP in kwargs:
|
||||||
color_t = kwargs[ATTR_COLOR_TEMP]
|
color_t = kwargs[ATTR_COLOR_TEMP]
|
||||||
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
|
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
|
||||||
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
|
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
|
||||||
self._light.set_temperature(kelvin, fade)
|
_LOGGER.debug("turn_on requested set_temperature for light:"
|
||||||
|
" %s: %s ", self._name, kelvin)
|
||||||
|
self._light.set_temperature(kelvin, transition)
|
||||||
|
|
||||||
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||||
|
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
|
||||||
|
self._name, self._brightness)
|
||||||
|
self._brightness = self._light.set_luminance(
|
||||||
|
int(self._brightness / 2.55),
|
||||||
|
transition)
|
||||||
|
|
||||||
|
if ATTR_EFFECT in kwargs:
|
||||||
effect = kwargs.get(ATTR_EFFECT)
|
effect = kwargs.get(ATTR_EFFECT)
|
||||||
if effect == EFFECT_RANDOM:
|
if effect == EFFECT_RANDOM:
|
||||||
self._light.set_rgb(random.randrange(0, 255),
|
self._light.set_rgb(random.randrange(0, 255),
|
||||||
random.randrange(0, 255),
|
random.randrange(0, 255),
|
||||||
random.randrange(0, 255),
|
random.randrange(0, 255),
|
||||||
fade)
|
transition)
|
||||||
|
_LOGGER.debug("turn_on requested random effect for light:"
|
||||||
|
" %s with transition %s ",
|
||||||
|
self._name, transition)
|
||||||
|
|
||||||
self._light.set_luminance(brightness, fade)
|
self.schedule_update_ha_state()
|
||||||
self.update_ha_state()
|
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
_LOGGER.debug("turn_off Attempting to turn off light: %s ",
|
||||||
|
self._name)
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
fade = kwargs[ATTR_TRANSITION] * 10
|
transition = kwargs[ATTR_TRANSITION] * 10
|
||||||
|
_LOGGER.debug("turn_off requested transition time for light:"
|
||||||
|
" %s is: %s ",
|
||||||
|
self._name, transition)
|
||||||
|
self._light.set_luminance(0, transition)
|
||||||
else:
|
else:
|
||||||
fade = 0
|
transition = 0
|
||||||
self._light.set_luminance(0, fade)
|
_LOGGER.debug("turn_off requested transition time for light:"
|
||||||
self.update_ha_state()
|
" %s is: %s ",
|
||||||
|
self._name, transition)
|
||||||
|
self._light.set_onoff(0)
|
||||||
|
self._state = self._light.on()
|
||||||
|
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Synchronize state with bridge."""
|
"""Synchronize state with bridge."""
|
||||||
self.update_lights(no_throttle=True)
|
self.update_lights(no_throttle=True)
|
||||||
|
self._brightness = int(self._light.lum() * 2.55)
|
||||||
|
self._name = self._light.name()
|
||||||
|
self._rgb = self._light.rgb()
|
||||||
|
o_temp = self._light.temp()
|
||||||
|
self._temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS)
|
||||||
|
* (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
|
||||||
|
self._state = self._light.on()
|
||||||
|
|
|
@ -84,7 +84,7 @@ class SCSGateLight(Light):
|
||||||
ToggleStatusTask(target=self._scs_id, toggled=True))
|
ToggleStatusTask(target=self._scs_id, toggled=True))
|
||||||
|
|
||||||
self._toggled = True
|
self._toggled = True
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
@ -94,7 +94,7 @@ class SCSGateLight(Light):
|
||||||
ToggleStatusTask(target=self._scs_id, toggled=False))
|
ToggleStatusTask(target=self._scs_id, toggled=False))
|
||||||
|
|
||||||
self._toggled = False
|
self._toggled = False
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def process_event(self, message):
|
def process_event(self, message):
|
||||||
"""Handle a SCSGate message related with this light."""
|
"""Handle a SCSGate message related with this light."""
|
||||||
|
|
|
@ -6,14 +6,14 @@ https://home-assistant.io/components/light.tellstick/
|
||||||
"""
|
"""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import tellstick
|
|
||||||
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
||||||
SUPPORT_BRIGHTNESS, Light)
|
SUPPORT_BRIGHTNESS, Light)
|
||||||
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
|
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
|
||||||
ATTR_DISCOVER_DEVICES,
|
ATTR_DISCOVER_DEVICES,
|
||||||
ATTR_DISCOVER_CONFIG)
|
ATTR_DISCOVER_CONFIG,
|
||||||
|
DOMAIN, TellstickDevice)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN})
|
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN})
|
||||||
|
|
||||||
SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
|
SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
|
@ -22,32 +22,25 @@ SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup Tellstick lights."""
|
"""Setup Tellstick lights."""
|
||||||
if (discovery_info is None or
|
if (discovery_info is None or
|
||||||
discovery_info[ATTR_DISCOVER_DEVICES] is None or
|
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||||
tellstick.TELLCORE_REGISTRY is None):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Allow platform level override, fallback to module config
|
||||||
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
|
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
|
||||||
DEFAULT_SIGNAL_REPETITIONS)
|
DEFAULT_SIGNAL_REPETITIONS)
|
||||||
|
|
||||||
add_devices(TellstickLight(
|
add_devices(TellstickLight(tellcore_id, signal_repetitions)
|
||||||
tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions)
|
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
|
||||||
for switch_id in discovery_info[ATTR_DISCOVER_DEVICES])
|
|
||||||
|
|
||||||
|
|
||||||
class TellstickLight(tellstick.TellstickDevice, Light):
|
class TellstickLight(TellstickDevice, Light):
|
||||||
"""Representation of a Tellstick light."""
|
"""Representation of a Tellstick light."""
|
||||||
|
|
||||||
def __init__(self, tellstick_device, signal_repetitions):
|
def __init__(self, tellcore_id, signal_repetitions):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
self._brightness = 255
|
super().__init__(tellcore_id, signal_repetitions)
|
||||||
tellstick.TellstickDevice.__init__(self,
|
|
||||||
tellstick_device,
|
|
||||||
signal_repetitions)
|
|
||||||
|
|
||||||
@property
|
self._brightness = 255
|
||||||
def is_on(self):
|
|
||||||
"""Return true if switch is on."""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
|
@ -59,37 +52,32 @@ class TellstickLight(tellstick.TellstickDevice, Light):
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
return SUPPORT_TELLSTICK
|
return SUPPORT_TELLSTICK
|
||||||
|
|
||||||
def set_tellstick_state(self, last_command_sent, last_data_sent):
|
def _parse_ha_data(self, kwargs):
|
||||||
"""Update the internal representation of the switch."""
|
"""Turn the value from HA into something useful."""
|
||||||
from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_DIM
|
return kwargs.get(ATTR_BRIGHTNESS)
|
||||||
if last_command_sent == TELLSTICK_DIM:
|
|
||||||
if last_data_sent is not None:
|
|
||||||
self._brightness = int(last_data_sent)
|
|
||||||
self._state = self._brightness > 0
|
|
||||||
else:
|
|
||||||
self._state = last_command_sent == TELLSTICK_TURNON
|
|
||||||
|
|
||||||
def _send_tellstick_command(self, command, data):
|
def _parse_tellcore_data(self, tellcore_data):
|
||||||
"""Handle the turn_on / turn_off commands."""
|
"""Turn the value recieved from tellcore into something useful."""
|
||||||
from tellcore.constants import (TELLSTICK_TURNOFF, TELLSTICK_DIM)
|
if tellcore_data is not None:
|
||||||
if command == TELLSTICK_TURNOFF:
|
brightness = int(tellcore_data)
|
||||||
self.tellstick_device.turn_off()
|
return brightness
|
||||||
elif command == TELLSTICK_DIM:
|
|
||||||
self.tellstick_device.dim(self._brightness)
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
return None
|
||||||
"Command not implemented: {}".format(command))
|
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def _update_model(self, new_state, data):
|
||||||
"""Turn the switch on."""
|
"""Update the device entity state to match the arguments."""
|
||||||
from tellcore.constants import TELLSTICK_DIM
|
if new_state:
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = data
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
self._brightness = brightness
|
self._brightness = brightness
|
||||||
|
|
||||||
self.call_tellstick(TELLSTICK_DIM, self._brightness)
|
self._state = (self._brightness > 0)
|
||||||
|
else:
|
||||||
|
self._state = False
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def _send_tellstick_command(self):
|
||||||
"""Turn the switch off."""
|
"""Let tellcore update the device to match the current state."""
|
||||||
from tellcore.constants import TELLSTICK_TURNOFF
|
if self._state:
|
||||||
self.call_tellstick(TELLSTICK_TURNOFF)
|
self._tellcore_device.dim(self._brightness)
|
||||||
|
else:
|
||||||
|
self._tellcore_device.turn_off()
|
||||||
|
|
|
@ -53,13 +53,13 @@ class VeraLight(VeraDevice, Light):
|
||||||
self.vera_device.switch_on()
|
self.vera_device.switch_on()
|
||||||
|
|
||||||
self._state = STATE_ON
|
self._state = STATE_ON
|
||||||
self.update_ha_state(True)
|
self.schedule_update_ha_state(True)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
self.vera_device.switch_off()
|
self.vera_device.switch_off()
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
|
|
@ -23,15 +23,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Wink lights."""
|
"""Setup the Wink lights."""
|
||||||
import pywink
|
import pywink
|
||||||
|
|
||||||
add_devices(WinkLight(light) for light in pywink.get_bulbs())
|
add_devices(WinkLight(light, hass) for light in pywink.get_bulbs())
|
||||||
|
|
||||||
|
|
||||||
class WinkLight(WinkDevice, Light):
|
class WinkLight(WinkDevice, Light):
|
||||||
"""Representation of a Wink light."""
|
"""Representation of a Wink light."""
|
||||||
|
|
||||||
def __init__(self, wink):
|
def __init__(self, wink, hass):
|
||||||
"""Initialize the Wink device."""
|
"""Initialize the Wink device."""
|
||||||
WinkDevice.__init__(self, wink)
|
WinkDevice.__init__(self, wink, hass)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
|
|
@ -15,15 +15,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Wink platform."""
|
"""Setup the Wink platform."""
|
||||||
import pywink
|
import pywink
|
||||||
|
|
||||||
add_devices(WinkLockDevice(lock) for lock in pywink.get_locks())
|
add_devices(WinkLockDevice(lock, hass) for lock in pywink.get_locks())
|
||||||
|
|
||||||
|
|
||||||
class WinkLockDevice(WinkDevice, LockDevice):
|
class WinkLockDevice(WinkDevice, LockDevice):
|
||||||
"""Representation of a Wink lock."""
|
"""Representation of a Wink lock."""
|
||||||
|
|
||||||
def __init__(self, wink):
|
def __init__(self, wink, hass):
|
||||||
"""Initialize the lock."""
|
"""Initialize the lock."""
|
||||||
WinkDevice.__init__(self, wink)
|
WinkDevice.__init__(self, wink, hass)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
|
|
|
@ -101,7 +101,7 @@ def setup(hass, config):
|
||||||
message = message.async_render()
|
message = message.async_render()
|
||||||
async_log_entry(hass, name, message, domain, entity_id)
|
async_log_entry(hass, name, message, domain, entity_id)
|
||||||
|
|
||||||
hass.http.register_view(LogbookView(hass, config))
|
hass.http.register_view(LogbookView(config))
|
||||||
|
|
||||||
register_built_in_panel(hass, 'logbook', 'Logbook',
|
register_built_in_panel(hass, 'logbook', 'Logbook',
|
||||||
'mdi:format-list-bulleted-type')
|
'mdi:format-list-bulleted-type')
|
||||||
|
@ -118,9 +118,8 @@ class LogbookView(HomeAssistantView):
|
||||||
name = 'api:logbook'
|
name = 'api:logbook'
|
||||||
extra_urls = ['/api/logbook/{datetime}']
|
extra_urls = ['/api/logbook/{datetime}']
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, config):
|
||||||
"""Initilalize the logbook view."""
|
"""Initilalize the logbook view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -140,13 +139,15 @@ class LogbookView(HomeAssistantView):
|
||||||
def get_results():
|
def get_results():
|
||||||
"""Query DB for results."""
|
"""Query DB for results."""
|
||||||
events = recorder.get_model('Events')
|
events = recorder.get_model('Events')
|
||||||
query = recorder.query('Events').filter(
|
query = recorder.query('Events').order_by(
|
||||||
|
events.time_fired).filter(
|
||||||
(events.time_fired > start_day) &
|
(events.time_fired > start_day) &
|
||||||
(events.time_fired < end_day))
|
(events.time_fired < end_day))
|
||||||
events = recorder.execute(query)
|
events = recorder.execute(query)
|
||||||
return _exclude_events(events, self.config)
|
return _exclude_events(events, self.config)
|
||||||
|
|
||||||
events = yield from self.hass.loop.run_in_executor(None, get_results)
|
events = yield from request.app['hass'].loop.run_in_executor(
|
||||||
|
None, get_results)
|
||||||
|
|
||||||
return self.json(humanify(events))
|
return self.json(humanify(events))
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -58,6 +59,8 @@ ATTR_MEDIA_SEEK_POSITION = 'seek_position'
|
||||||
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
|
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
|
||||||
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
|
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
|
||||||
ATTR_MEDIA_DURATION = 'media_duration'
|
ATTR_MEDIA_DURATION = 'media_duration'
|
||||||
|
ATTR_MEDIA_POSITION = 'media_position'
|
||||||
|
ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at'
|
||||||
ATTR_MEDIA_TITLE = 'media_title'
|
ATTR_MEDIA_TITLE = 'media_title'
|
||||||
ATTR_MEDIA_ARTIST = 'media_artist'
|
ATTR_MEDIA_ARTIST = 'media_artist'
|
||||||
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
|
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
|
||||||
|
@ -119,6 +122,8 @@ ATTR_TO_PROPERTY = [
|
||||||
ATTR_MEDIA_CONTENT_ID,
|
ATTR_MEDIA_CONTENT_ID,
|
||||||
ATTR_MEDIA_CONTENT_TYPE,
|
ATTR_MEDIA_CONTENT_TYPE,
|
||||||
ATTR_MEDIA_DURATION,
|
ATTR_MEDIA_DURATION,
|
||||||
|
ATTR_MEDIA_POSITION,
|
||||||
|
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||||
ATTR_MEDIA_TITLE,
|
ATTR_MEDIA_TITLE,
|
||||||
ATTR_MEDIA_ARTIST,
|
ATTR_MEDIA_ARTIST,
|
||||||
ATTR_MEDIA_ALBUM_NAME,
|
ATTR_MEDIA_ALBUM_NAME,
|
||||||
|
@ -304,7 +309,7 @@ def setup(hass, config):
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
hass.http.register_view(MediaPlayerImageView(hass, component.entities))
|
hass.http.register_view(MediaPlayerImageView(component.entities))
|
||||||
|
|
||||||
component.setup(config)
|
component.setup(config)
|
||||||
|
|
||||||
|
@ -446,6 +451,19 @@ class MediaPlayerDevice(Entity):
|
||||||
"""Duration of current playing media in seconds."""
|
"""Duration of current playing media in seconds."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
"""Position of current playing media in seconds."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""When was the position of the current playing media valid.
|
||||||
|
|
||||||
|
Returns value from homeassistant.util.dt.utcnow().
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
|
@ -704,17 +722,25 @@ def _async_fetch_image(hass, url):
|
||||||
return cache_images[url]
|
return cache_images[url]
|
||||||
|
|
||||||
content, content_type = (None, None)
|
content, content_type = (None, None)
|
||||||
|
websession = async_get_clientsession(hass)
|
||||||
|
response = None
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=hass.loop):
|
with async_timeout.timeout(10, loop=hass.loop):
|
||||||
response = yield from hass.websession.get(url)
|
response = yield from websession.get(url)
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
content = yield from response.read()
|
content = yield from response.read()
|
||||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||||
yield from response.release()
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if content:
|
finally:
|
||||||
|
if response is not None:
|
||||||
|
yield from response.release()
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
cache_images[url] = (content, content_type)
|
cache_images[url] = (content, content_type)
|
||||||
cache_urls.append(url)
|
cache_urls.append(url)
|
||||||
|
|
||||||
|
@ -736,9 +762,8 @@ class MediaPlayerImageView(HomeAssistantView):
|
||||||
url = "/api/media_player_proxy/{entity_id}"
|
url = "/api/media_player_proxy/{entity_id}"
|
||||||
name = "api:media_player:image"
|
name = "api:media_player:image"
|
||||||
|
|
||||||
def __init__(self, hass, entities):
|
def __init__(self, entities):
|
||||||
"""Initialize a media player view."""
|
"""Initialize a media player view."""
|
||||||
super().__init__(hass)
|
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -748,14 +773,14 @@ class MediaPlayerImageView(HomeAssistantView):
|
||||||
if player is None:
|
if player is None:
|
||||||
return web.Response(status=404)
|
return web.Response(status=404)
|
||||||
|
|
||||||
authenticated = (request.authenticated or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.GET.get('token') == player.access_token)
|
request.GET.get('token') == player.access_token)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
return web.Response(status=401)
|
return web.Response(status=401)
|
||||||
|
|
||||||
data, content_type = yield from _async_fetch_image(
|
data, content_type = yield from _async_fetch_image(
|
||||||
self.hass, player.media_image_url)
|
request.app['hass'], player.media_image_url)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return web.Response(status=500)
|
return web.Response(status=500)
|
||||||
|
|
|
@ -81,6 +81,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
KNOWN_HOSTS.append(host)
|
KNOWN_HOSTS.append(host)
|
||||||
except pychromecast.ChromecastConnectionError:
|
except pychromecast.ChromecastConnectionError:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# add the device anyway, get_chromecasts couldn't find it
|
||||||
|
casts.append(CastDevice(pychromecast.Chromecast(*host)))
|
||||||
|
KNOWN_HOSTS.append(host)
|
||||||
|
except pychromecast.ChromecastConnectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
add_devices(casts)
|
add_devices(casts)
|
||||||
|
|
||||||
|
|
33
homeassistant/components/media_player/denon.py
Normal file → Executable file
33
homeassistant/components/media_player/denon.py
Normal file → Executable file
|
@ -10,8 +10,9 @@ import telnetlib
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
|
||||||
|
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
MediaPlayerDevice)
|
MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
||||||
|
@ -21,8 +22,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Music station'
|
DEFAULT_NAME = 'Music station'
|
||||||
|
|
||||||
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
|
SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
@ -51,6 +53,8 @@ class DenonDevice(MediaPlayerDevice):
|
||||||
self._host = host
|
self._host = host
|
||||||
self._pwstate = 'PWSTANDBY'
|
self._pwstate = 'PWSTANDBY'
|
||||||
self._volume = 0
|
self._volume = 0
|
||||||
|
self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER',
|
||||||
|
'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'}
|
||||||
self._muted = False
|
self._muted = False
|
||||||
self._mediasource = ''
|
self._mediasource = ''
|
||||||
|
|
||||||
|
@ -58,7 +62,14 @@ class DenonDevice(MediaPlayerDevice):
|
||||||
def telnet_request(cls, telnet, command):
|
def telnet_request(cls, telnet, command):
|
||||||
"""Execute `command` and return the response."""
|
"""Execute `command` and return the response."""
|
||||||
telnet.write(command.encode('ASCII') + b'\r')
|
telnet.write(command.encode('ASCII') + b'\r')
|
||||||
return telnet.read_until(b'\r', timeout=0.2).decode('ASCII').strip()
|
lines = []
|
||||||
|
while True:
|
||||||
|
line = telnet.read_until(b'\r', timeout=0.2)
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
lines.append(line.decode('ASCII').strip())
|
||||||
|
|
||||||
|
return lines[0]
|
||||||
|
|
||||||
def telnet_command(self, command):
|
def telnet_command(self, command):
|
||||||
"""Establish a telnet connection and sends `command`."""
|
"""Establish a telnet connection and sends `command`."""
|
||||||
|
@ -75,9 +86,6 @@ class DenonDevice(MediaPlayerDevice):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._pwstate = self.telnet_request(telnet, 'PW?')
|
self._pwstate = self.telnet_request(telnet, 'PW?')
|
||||||
# PW? sends also SISTATUS, which is not interesting
|
|
||||||
telnet.read_until(b"\r", timeout=0.2)
|
|
||||||
|
|
||||||
volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):]
|
volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):]
|
||||||
self._volume = int(volume_str) / 60
|
self._volume = int(volume_str) / 60
|
||||||
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
|
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
|
||||||
|
@ -111,6 +119,11 @@ class DenonDevice(MediaPlayerDevice):
|
||||||
"""Boolean if volume is currently muted."""
|
"""Boolean if volume is currently muted."""
|
||||||
return self._muted
|
return self._muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_list(self):
|
||||||
|
"""List of available input sources."""
|
||||||
|
return list(self._source_list.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Current media source."""
|
"""Current media source."""
|
||||||
|
@ -161,3 +174,7 @@ class DenonDevice(MediaPlayerDevice):
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
self.telnet_command('PWON')
|
self.telnet_command('PWON')
|
||||||
|
|
||||||
|
def select_source(self, source):
|
||||||
|
"""Select input source."""
|
||||||
|
self.telnet_command(self._source_list.get(source))
|
||||||
|
|
245
homeassistant/components/media_player/denonavr.py
Normal file
245
homeassistant/components/media_player/denonavr.py
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
"""
|
||||||
|
Support for Denon AVR receivers using their HTTP interface.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.denon/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
|
||||||
|
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
|
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
||||||
|
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
|
||||||
|
MEDIA_TYPE_MUSIC)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
||||||
|
CONF_NAME, STATE_ON)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['denonavr==0.1.6']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = None
|
||||||
|
|
||||||
|
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
|
||||||
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||||
|
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \
|
||||||
|
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
|
SUPPORT_NEXT_TRACK
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Denon platform."""
|
||||||
|
import denonavr
|
||||||
|
|
||||||
|
receiver = denonavr.DenonAVR(config.get(CONF_HOST), config.get(CONF_NAME))
|
||||||
|
|
||||||
|
add_devices([DenonDevice(receiver)])
|
||||||
|
_LOGGER.info("Denon receiver at host %s initialized",
|
||||||
|
config.get(CONF_HOST))
|
||||||
|
|
||||||
|
|
||||||
|
class DenonDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a Denon Media Player Device."""
|
||||||
|
|
||||||
|
def __init__(self, receiver):
|
||||||
|
"""Initialize the device."""
|
||||||
|
self._receiver = receiver
|
||||||
|
self._name = self._receiver.name
|
||||||
|
self._muted = self._receiver.muted
|
||||||
|
self._volume = self._receiver.volume
|
||||||
|
self._current_source = self._receiver.input_func
|
||||||
|
self._source_list = self._receiver.input_func_list
|
||||||
|
self._state = self._receiver.state
|
||||||
|
self._power = self._receiver.power
|
||||||
|
self._media_image_url = self._receiver.image_url
|
||||||
|
self._title = self._receiver.title
|
||||||
|
self._artist = self._receiver.artist
|
||||||
|
self._album = self._receiver.album
|
||||||
|
self._band = self._receiver.band
|
||||||
|
self._frequency = self._receiver.frequency
|
||||||
|
self._station = self._receiver.station
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest status information from device."""
|
||||||
|
# Update denonavr
|
||||||
|
self._receiver.update()
|
||||||
|
# Refresh own data
|
||||||
|
self._name = self._receiver.name
|
||||||
|
self._muted = self._receiver.muted
|
||||||
|
self._volume = self._receiver.volume
|
||||||
|
self._current_source = self._receiver.input_func
|
||||||
|
self._source_list = self._receiver.input_func_list
|
||||||
|
self._state = self._receiver.state
|
||||||
|
self._power = self._receiver.power
|
||||||
|
self._media_image_url = self._receiver.image_url
|
||||||
|
self._title = self._receiver.title
|
||||||
|
self._artist = self._receiver.artist
|
||||||
|
self._album = self._receiver.album
|
||||||
|
self._band = self._receiver.band
|
||||||
|
self._frequency = self._receiver.frequency
|
||||||
|
self._station = self._receiver.station
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Boolean if volume is currently muted."""
|
||||||
|
return self._muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
# Volume is send in a format like -50.0. Minimum is around -80.0
|
||||||
|
return (float(self._volume) + 80) / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self):
|
||||||
|
"""Return the current input source."""
|
||||||
|
return self._current_source
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_list(self):
|
||||||
|
"""List of available input sources."""
|
||||||
|
return self._source_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_DENON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_id(self):
|
||||||
|
"""Content ID of current playing media."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_type(self):
|
||||||
|
"""Content type of current playing media."""
|
||||||
|
if self._state == STATE_PLAYING or self._state == STATE_PAUSED:
|
||||||
|
return MEDIA_TYPE_MUSIC
|
||||||
|
else:
|
||||||
|
return MEDIA_TYPE_CHANNEL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_duration(self):
|
||||||
|
"""Duration of current playing media in seconds."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_image_url(self):
|
||||||
|
"""Image url of current playing media."""
|
||||||
|
if self._power == "ON":
|
||||||
|
return self._media_image_url
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_title(self):
|
||||||
|
"""Title of current playing media."""
|
||||||
|
if self._title is not None:
|
||||||
|
return self._title
|
||||||
|
else:
|
||||||
|
return self._frequency
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_artist(self):
|
||||||
|
"""Artist of current playing media, music track only."""
|
||||||
|
if self._artist is not None:
|
||||||
|
return self._artist
|
||||||
|
else:
|
||||||
|
return self._band
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_album_name(self):
|
||||||
|
"""Album name of current playing media, music track only."""
|
||||||
|
if self._album is not None:
|
||||||
|
return self._album
|
||||||
|
else:
|
||||||
|
return self._station
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_album_artist(self):
|
||||||
|
"""Album artist of current playing media, music track only."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_track(self):
|
||||||
|
"""Track number of current playing media, music track only."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_series_title(self):
|
||||||
|
"""Title of series of current playing media, TV show only."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_season(self):
|
||||||
|
"""Season of current playing media, TV show only."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_episode(self):
|
||||||
|
"""Episode of current playing media, TV show only."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def media_play_pause(self):
|
||||||
|
"""Simulate play pause media player."""
|
||||||
|
return self._receiver.toggle_play_pause()
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
"""Send previous track command."""
|
||||||
|
return self._receiver.previous_track()
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
"""Send next track command."""
|
||||||
|
return self._receiver.next_track()
|
||||||
|
|
||||||
|
def select_source(self, source):
|
||||||
|
"""Select input source."""
|
||||||
|
return self._receiver.set_input_func(source)
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn on media player."""
|
||||||
|
if self._receiver.power_on():
|
||||||
|
self._state = STATE_ON
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off media player."""
|
||||||
|
if self._receiver.power_off():
|
||||||
|
self._state = STATE_OFF
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def volume_up(self):
|
||||||
|
"""Volume up the media player."""
|
||||||
|
return self._receiver.volume_up()
|
||||||
|
|
||||||
|
def volume_down(self):
|
||||||
|
"""Volume down media player."""
|
||||||
|
return self._receiver.volume_down()
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Send mute command."""
|
||||||
|
return self._receiver.mute(mute)
|
171
homeassistant/components/media_player/dunehd.py
Normal file
171
homeassistant/components/media_player/dunehd.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
"""
|
||||||
|
DuneHD implementation of the media player.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/media_player.dunehd/
|
||||||
|
"""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_NEXT_TRACK,
|
||||||
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA,
|
||||||
|
MediaPlayerDevice)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_ON, STATE_PLAYING)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pdunehd==1.3']
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'DuneHD'
|
||||||
|
|
||||||
|
CONF_SOURCES = 'sources'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_SOURCES): cv.ordered_dict(cv.string, cv.string),
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
DUNEHD_PLAYER_SUPPORT = \
|
||||||
|
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||||
|
SUPPORT_SELECT_SOURCE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up the media player demo platform."""
|
||||||
|
sources = config.get(CONF_SOURCES, {})
|
||||||
|
|
||||||
|
from pdunehd import DuneHDPlayer
|
||||||
|
add_devices([DuneHDPlayerEntity(
|
||||||
|
DuneHDPlayer(config[CONF_HOST]),
|
||||||
|
config[CONF_NAME],
|
||||||
|
sources)])
|
||||||
|
|
||||||
|
|
||||||
|
class DuneHDPlayerEntity(MediaPlayerDevice):
|
||||||
|
"""Implementation of the Dune HD player."""
|
||||||
|
|
||||||
|
def __init__(self, player, name, sources):
|
||||||
|
"""Setup entity to control Dune HD."""
|
||||||
|
self._player = player
|
||||||
|
self._name = name
|
||||||
|
self._sources = sources
|
||||||
|
self._media_title = None
|
||||||
|
self._state = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update internal status of the entity."""
|
||||||
|
self._state = self._player.update_state()
|
||||||
|
self.__update_title()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return player state."""
|
||||||
|
state = STATE_OFF
|
||||||
|
if 'playback_position' in self._state:
|
||||||
|
state = STATE_PLAYING
|
||||||
|
if self._state['player_state'] in ('playing', 'buffering'):
|
||||||
|
state = STATE_PLAYING
|
||||||
|
if int(self._state.get('playback_speed', 1234)) == 0:
|
||||||
|
state = STATE_PAUSED
|
||||||
|
if self._state['player_state'] == 'navigator':
|
||||||
|
state = STATE_ON
|
||||||
|
return state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
return int(self._state.get('playback_volume', 0)) / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Boolean if volume is currently muted."""
|
||||||
|
return int(self._state.get('playback_mute', 0)) == 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_list(self):
|
||||||
|
"""List of available input sources."""
|
||||||
|
return list(self._sources.keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return DUNEHD_PLAYER_SUPPORT
|
||||||
|
|
||||||
|
def volume_up(self):
|
||||||
|
"""Volume up media player."""
|
||||||
|
self._state = self._player.volume_up()
|
||||||
|
|
||||||
|
def volume_down(self):
|
||||||
|
"""Volume down media player."""
|
||||||
|
self._state = self._player.volume_down()
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Mute/unmute player volume."""
|
||||||
|
self._state = self._player.mute(mute)
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off media player."""
|
||||||
|
self._media_title = None
|
||||||
|
self._state = self._player.turn_off()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn off media player."""
|
||||||
|
self._state = self._player.turn_on()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
"""Play media media player."""
|
||||||
|
self._state = self._player.play()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
"""Pause media player."""
|
||||||
|
self._state = self._player.pause()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_title(self):
|
||||||
|
"""Current media source."""
|
||||||
|
self.__update_title()
|
||||||
|
if self._media_title:
|
||||||
|
return self._media_title
|
||||||
|
return self._state.get('playback_url', 'Not playing')
|
||||||
|
|
||||||
|
def __update_title(self):
|
||||||
|
if self._state['player_state'] == 'bluray_playback':
|
||||||
|
self._media_title = 'Blu-Ray'
|
||||||
|
elif 'playback_url' in self._state:
|
||||||
|
sources = self._sources
|
||||||
|
sval = sources.values()
|
||||||
|
skey = sources.keys()
|
||||||
|
pburl = self._state['playback_url']
|
||||||
|
if pburl in sval:
|
||||||
|
self._media_title = list(skey)[list(sval).index(pburl)]
|
||||||
|
else:
|
||||||
|
self._media_title = pburl
|
||||||
|
|
||||||
|
def select_source(self, source):
|
||||||
|
"""Select input source."""
|
||||||
|
self._media_title = source
|
||||||
|
self._state = self._player.launch_media_url(self._sources.get(source))
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
"""Send previous track command."""
|
||||||
|
self._state = self._player.previous_track()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
"""Send next track command."""
|
||||||
|
self._state = self._player.next_track()
|
||||||
|
self.schedule_update_ha_state()
|
|
@ -100,7 +100,7 @@ class MpdDevice(MediaPlayerDevice):
|
||||||
try:
|
try:
|
||||||
self.status = self.client.status()
|
self.status = self.client.status()
|
||||||
self.currentsong = self.client.currentsong()
|
self.currentsong = self.client.currentsong()
|
||||||
except (mpd.ConnectionError, BrokenPipeError, ValueError):
|
except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError):
|
||||||
# Cleanly disconnect in case connection is not in valid state
|
# Cleanly disconnect in case connection is not in valid state
|
||||||
try:
|
try:
|
||||||
self.client.disconnect()
|
self.client.disconnect()
|
||||||
|
@ -133,7 +133,7 @@ class MpdDevice(MediaPlayerDevice):
|
||||||
@property
|
@property
|
||||||
def media_content_id(self):
|
def media_content_id(self):
|
||||||
"""Content ID of current playing media."""
|
"""Content ID of current playing media."""
|
||||||
return self.currentsong['id']
|
return self.currentsong.get('file')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_content_type(self):
|
def media_content_type(self):
|
||||||
|
|
|
@ -120,7 +120,7 @@ class PandoraMediaPlayer(MediaPlayerDevice):
|
||||||
self.update_playing_status()
|
self.update_playing_status()
|
||||||
|
|
||||||
self._player_state = STATE_IDLE
|
self._player_state = STATE_IDLE
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn the media player off."""
|
"""Turn the media player off."""
|
||||||
|
@ -138,24 +138,24 @@ class PandoraMediaPlayer(MediaPlayerDevice):
|
||||||
_LOGGER.info('Killed Pianobar subprocess')
|
_LOGGER.info('Killed Pianobar subprocess')
|
||||||
self._pianobar = None
|
self._pianobar = None
|
||||||
self._player_state = STATE_OFF
|
self._player_state = STATE_OFF
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
||||||
self._player_state = STATE_PLAYING
|
self._player_state = STATE_PLAYING
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
||||||
self._player_state = STATE_PAUSED
|
self._player_state = STATE_PAUSED
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Go to next track."""
|
"""Go to next track."""
|
||||||
self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK)
|
self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK)
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
|
@ -350,6 +350,8 @@ class PandoraMediaPlayer(MediaPlayerDevice):
|
||||||
pass
|
pass
|
||||||
except pexpect.exceptions.TIMEOUT:
|
except pexpect.exceptions.TIMEOUT:
|
||||||
pass
|
pass
|
||||||
|
except pexpect.exceptions.EOF:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _pianobar_exists():
|
def _pianobar_exists():
|
||||||
|
|
|
@ -9,13 +9,14 @@ from datetime import timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
|
||||||
PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
|
|
||||||
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, MediaPlayerDevice)
|
|
||||||
from homeassistant.const import (
|
|
||||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_HOST, CONF_NAME)
|
|
||||||
from homeassistant.util import Throttle
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
|
||||||
|
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE,
|
||||||
|
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['ha-philipsjs==0.0.1']
|
REQUIREMENTS = ['ha-philipsjs==0.0.1']
|
||||||
|
|
||||||
|
@ -26,6 +27,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||||
SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
|
SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE
|
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
|
SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \
|
||||||
|
SUPPORT_PREVIOUS_TRACK
|
||||||
|
|
||||||
DEFAULT_DEVICE = 'default'
|
DEFAULT_DEVICE = 'default'
|
||||||
DEFAULT_HOST = '127.0.0.1'
|
DEFAULT_HOST = '127.0.0.1'
|
||||||
DEFAULT_NAME = 'Philips TV'
|
DEFAULT_NAME = 'Philips TV'
|
||||||
|
@ -68,6 +72,8 @@ class PhilipsTV(MediaPlayerDevice):
|
||||||
self._source_list = []
|
self._source_list = []
|
||||||
self._connfail = 0
|
self._connfail = 0
|
||||||
self._source_mapping = {}
|
self._source_mapping = {}
|
||||||
|
self._watching_tv = None
|
||||||
|
self._channel_name = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -82,6 +88,9 @@ class PhilipsTV(MediaPlayerDevice):
|
||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
|
if self._watching_tv:
|
||||||
|
return SUPPORT_PHILIPS_JS_TV
|
||||||
|
else:
|
||||||
return SUPPORT_PHILIPS_JS
|
return SUPPORT_PHILIPS_JS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -106,6 +115,7 @@ class PhilipsTV(MediaPlayerDevice):
|
||||||
self._source = source
|
self._source = source
|
||||||
if not self._tv.on:
|
if not self._tv.on:
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
self._watching_tv = bool(self._tv.source_id == 'tv')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
|
@ -141,9 +151,23 @@ class PhilipsTV(MediaPlayerDevice):
|
||||||
if not self._tv.on:
|
if not self._tv.on:
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
"""Send rewind commmand."""
|
||||||
|
self._tv.sendKey('Previous')
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
"""Send fast forward commmand."""
|
||||||
|
self._tv.sendKey('Next')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Title of current playing media."""
|
"""Title of current playing media."""
|
||||||
|
if self._watching_tv:
|
||||||
|
if self._channel_name:
|
||||||
|
return '{} - {}'.format(self._source, self._channel_name)
|
||||||
|
else:
|
||||||
|
return self._source
|
||||||
|
else:
|
||||||
return self._source
|
return self._source
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
@ -167,3 +191,12 @@ class PhilipsTV(MediaPlayerDevice):
|
||||||
self._state = STATE_ON
|
self._state = STATE_ON
|
||||||
else:
|
else:
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
self._watching_tv = bool(self._tv.source_id == 'tv')
|
||||||
|
|
||||||
|
self._tv.getChannelId()
|
||||||
|
self._tv.getChannels()
|
||||||
|
if self._tv.channels and self._tv.channel_id in self._tv.channels:
|
||||||
|
self._channel_name = self._tv.channels[self._tv.channel_id]['name']
|
||||||
|
else:
|
||||||
|
self._channel_name = None
|
||||||
|
|
|
@ -204,3 +204,36 @@ sonos_clear_sleep_timer:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name(s) of entites that will have the timer cleared.
|
description: Name(s) of entites that will have the timer cleared.
|
||||||
example: 'media_player.living_room_sonos'
|
example: 'media_player.living_room_sonos'
|
||||||
|
|
||||||
|
|
||||||
|
soundtouch_play_everywhere:
|
||||||
|
description: Play on all Bose Soundtouch devices
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices
|
||||||
|
example: 'media_player.soundtouch_home'
|
||||||
|
|
||||||
|
soundtouch_create_zone:
|
||||||
|
description: Create a multi-room zone
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of entites that will coordinate the multi-room zone. Platform dependent.
|
||||||
|
example: 'media_player.soundtouch_home'
|
||||||
|
|
||||||
|
soundtouch_add_zone_slave:
|
||||||
|
description: Add a slave to a multi-room zone
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of entites that will be added to the multi-room zone. Platform dependent.
|
||||||
|
example: 'media_player.soundtouch_home'
|
||||||
|
|
||||||
|
soundtouch_remove_zone_slave:
|
||||||
|
description: Remove a slave from the multi-room zone
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of entites that will be remove from the multi-room zone. Platform dependent.
|
||||||
|
example: 'media_player.soundtouch_home'
|
|
@ -15,11 +15,13 @@ from homeassistant.components.media_player import (
|
||||||
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
||||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID)
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
|
||||||
|
CONF_HOSTS)
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
REQUIREMENTS = ['SoCo==0.12']
|
REQUIREMENTS = ['SoCo==0.12']
|
||||||
|
|
||||||
|
@ -48,9 +50,18 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
|
||||||
SUPPORT_SOURCE_LINEIN = 'Line-in'
|
SUPPORT_SOURCE_LINEIN = 'Line-in'
|
||||||
SUPPORT_SOURCE_TV = 'TV'
|
SUPPORT_SOURCE_TV = 'TV'
|
||||||
|
|
||||||
|
CONF_ADVERTISE_ADDR = 'advertise_addr'
|
||||||
|
CONF_INTERFACE_ADDR = 'interface_addr'
|
||||||
|
|
||||||
# Service call validation schemas
|
# Service call validation schemas
|
||||||
ATTR_SLEEP_TIME = 'sleep_time'
|
ATTR_SLEEP_TIME = 'sleep_time'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
|
||||||
|
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
|
||||||
|
vol.Optional(CONF_HOSTS): cv.ensure_list(cv.string),
|
||||||
|
})
|
||||||
|
|
||||||
SONOS_SCHEMA = vol.Schema({
|
SONOS_SCHEMA = vol.Schema({
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
})
|
})
|
||||||
|
@ -69,6 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
import soco
|
import soco
|
||||||
global DEVICES
|
global DEVICES
|
||||||
|
|
||||||
|
advertise_addr = config.get(CONF_ADVERTISE_ADDR, None)
|
||||||
|
if advertise_addr:
|
||||||
|
soco.config.EVENT_ADVERTISE_IP = advertise_addr
|
||||||
|
|
||||||
if discovery_info:
|
if discovery_info:
|
||||||
player = soco.SoCo(discovery_info)
|
player = soco.SoCo(discovery_info)
|
||||||
|
|
||||||
|
@ -86,18 +101,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
players = None
|
players = None
|
||||||
hosts = config.get('hosts', None)
|
hosts = config.get(CONF_HOSTS, None)
|
||||||
if hosts:
|
if hosts:
|
||||||
# Support retro compatibility with comma separated list of hosts
|
# Support retro compatibility with comma separated list of hosts
|
||||||
# from config
|
# from config
|
||||||
|
hosts = hosts[0] if len(hosts) == 1 else hosts
|
||||||
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
|
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
|
||||||
players = []
|
players = []
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
players.append(soco.SoCo(socket.gethostbyname(host)))
|
players.append(soco.SoCo(socket.gethostbyname(host)))
|
||||||
|
|
||||||
if not players:
|
if not players:
|
||||||
players = soco.discover(interface_addr=config.get('interface_addr',
|
players = soco.discover(interface_addr=config.get(CONF_INTERFACE_ADDR))
|
||||||
None))
|
|
||||||
|
|
||||||
if not players:
|
if not players:
|
||||||
_LOGGER.warning('No Sonos speakers found.')
|
_LOGGER.warning('No Sonos speakers found.')
|
||||||
|
@ -264,6 +279,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._coordinator = None
|
self._coordinator = None
|
||||||
self._media_content_id = None
|
self._media_content_id = None
|
||||||
self._media_duration = None
|
self._media_duration = None
|
||||||
|
self._media_position = None
|
||||||
|
self._media_position_updated_at = None
|
||||||
self._media_image_url = None
|
self._media_image_url = None
|
||||||
self._media_artist = None
|
self._media_artist = None
|
||||||
self._media_album_name = None
|
self._media_album_name = None
|
||||||
|
@ -404,6 +421,9 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
media_album_name = track_info.get('album')
|
media_album_name = track_info.get('album')
|
||||||
media_title = track_info.get('title')
|
media_title = track_info.get('title')
|
||||||
|
|
||||||
|
media_position = None
|
||||||
|
media_position_updated_at = None
|
||||||
|
|
||||||
is_radio_stream = \
|
is_radio_stream = \
|
||||||
current_media_uri.startswith('x-sonosapi-stream:') or \
|
current_media_uri.startswith('x-sonosapi-stream:') or \
|
||||||
current_media_uri.startswith('x-rincon-mp3radio:')
|
current_media_uri.startswith('x-rincon-mp3radio:')
|
||||||
|
@ -425,7 +445,6 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
media_image_url = None
|
media_image_url = None
|
||||||
|
|
||||||
elif is_radio_stream:
|
elif is_radio_stream:
|
||||||
is_radio_stream = True
|
|
||||||
media_image_url = self._format_media_image_url(
|
media_image_url = self._format_media_image_url(
|
||||||
current_media_uri
|
current_media_uri
|
||||||
)
|
)
|
||||||
|
@ -489,6 +508,46 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
support_next_track = True
|
support_next_track = True
|
||||||
support_pause = True
|
support_pause = True
|
||||||
|
|
||||||
|
position_info = self._player.avTransport.GetPositionInfo(
|
||||||
|
[('InstanceID', 0),
|
||||||
|
('Channel', 'Master')]
|
||||||
|
)
|
||||||
|
rel_time = _parse_timespan(
|
||||||
|
position_info.get("RelTime")
|
||||||
|
)
|
||||||
|
|
||||||
|
# player no longer reports position?
|
||||||
|
update_media_position = rel_time is None and \
|
||||||
|
self._media_position is not None
|
||||||
|
|
||||||
|
# player started reporting position?
|
||||||
|
update_media_position |= rel_time is not None and \
|
||||||
|
self._media_position is None
|
||||||
|
|
||||||
|
# position changed?
|
||||||
|
if rel_time is not None and \
|
||||||
|
self._media_position is not None:
|
||||||
|
|
||||||
|
time_diff = utcnow() - self._media_position_updated_at
|
||||||
|
time_diff = time_diff.total_seconds()
|
||||||
|
|
||||||
|
calculated_position = \
|
||||||
|
self._media_position + \
|
||||||
|
time_diff
|
||||||
|
|
||||||
|
update_media_position = \
|
||||||
|
abs(calculated_position - rel_time) > 1.5
|
||||||
|
|
||||||
|
if update_media_position:
|
||||||
|
media_position = rel_time
|
||||||
|
media_position_updated_at = utcnow()
|
||||||
|
else:
|
||||||
|
# don't update media_position (don't want unneeded
|
||||||
|
# state transitions)
|
||||||
|
media_position = self._media_position
|
||||||
|
media_position_updated_at = \
|
||||||
|
self._media_position_updated_at
|
||||||
|
|
||||||
playlist_position = track_info.get('playlist_position')
|
playlist_position = track_info.get('playlist_position')
|
||||||
if playlist_position in ('', 'NOT_IMPLEMENTED', None):
|
if playlist_position in ('', 'NOT_IMPLEMENTED', None):
|
||||||
playlist_position = None
|
playlist_position = None
|
||||||
|
@ -514,6 +573,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._media_duration = _parse_timespan(
|
self._media_duration = _parse_timespan(
|
||||||
track_info.get('duration')
|
track_info.get('duration')
|
||||||
)
|
)
|
||||||
|
self._media_position = media_position
|
||||||
|
self._media_position_updated_at = media_position_updated_at
|
||||||
self._media_image_url = media_image_url
|
self._media_image_url = media_image_url
|
||||||
self._media_artist = media_artist
|
self._media_artist = media_artist
|
||||||
self._media_album_name = media_album_name
|
self._media_album_name = media_album_name
|
||||||
|
@ -541,6 +602,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._coordinator = None
|
self._coordinator = None
|
||||||
self._media_content_id = None
|
self._media_content_id = None
|
||||||
self._media_duration = None
|
self._media_duration = None
|
||||||
|
self._media_position = None
|
||||||
|
self._media_position_updated_at = None
|
||||||
self._media_image_url = None
|
self._media_image_url = None
|
||||||
self._media_artist = None
|
self._media_artist = None
|
||||||
self._media_album_name = None
|
self._media_album_name = None
|
||||||
|
@ -642,6 +705,25 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
else:
|
else:
|
||||||
return self._media_duration
|
return self._media_duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
"""Position of current playing media in seconds."""
|
||||||
|
if self._coordinator:
|
||||||
|
return self._coordinator.media_position
|
||||||
|
else:
|
||||||
|
return self._media_position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""When was the position of the current playing media valid.
|
||||||
|
|
||||||
|
Returns value from homeassistant.util.dt.utcnow().
|
||||||
|
"""
|
||||||
|
if self._coordinator:
|
||||||
|
return self._coordinator.media_position_updated_at
|
||||||
|
else:
|
||||||
|
return self._media_position_updated_at
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
|
|
393
homeassistant/components/media_player/soundtouch.py
Normal file
393
homeassistant/components/media_player/soundtouch.py
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
"""Support for interface with a Bose Soundtouch."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||||
|
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
|
SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
|
||||||
|
STATE_PAUSED, STATE_PLAYING,
|
||||||
|
STATE_UNAVAILABLE)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['libsoundtouch==0.1.0']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = 'media_player'
|
||||||
|
SERVICE_PLAY_EVERYWHERE = 'soundtouch_play_everywhere'
|
||||||
|
SERVICE_CREATE_ZONE = 'soundtouch_create_zone'
|
||||||
|
SERVICE_ADD_ZONE_SLAVE = 'soundtouch_add_zone_slave'
|
||||||
|
SERVICE_REMOVE_ZONE_SLAVE = 'soundtouch_remove_zone_slave'
|
||||||
|
|
||||||
|
MAP_STATUS = {
|
||||||
|
"PLAY_STATE": STATE_PLAYING,
|
||||||
|
"BUFFERING_STATE": STATE_PLAYING,
|
||||||
|
"PAUSE_STATE": STATE_PAUSED,
|
||||||
|
"STOp_STATE": STATE_OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({
|
||||||
|
'master': cv.entity_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({
|
||||||
|
'master': cv.entity_id,
|
||||||
|
'slaves': cv.entity_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({
|
||||||
|
'master': cv.entity_id,
|
||||||
|
'slaves': cv.entity_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({
|
||||||
|
'master': cv.entity_id,
|
||||||
|
'slaves': cv.entity_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Bose Soundtouch'
|
||||||
|
DEFAULT_PORT = 8090
|
||||||
|
|
||||||
|
DEVICES = []
|
||||||
|
|
||||||
|
SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||||
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
|
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
|
||||||
|
SUPPORT_VOLUME_SET | SUPPORT_TURN_ON
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Bose Soundtouch platform."""
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
|
||||||
|
remote_config = {
|
||||||
|
'name': 'HomeAssistant',
|
||||||
|
'description': config.get(CONF_NAME),
|
||||||
|
'id': 'ha.component.soundtouch',
|
||||||
|
'port': config.get(CONF_PORT),
|
||||||
|
'host': config.get(CONF_HOST)
|
||||||
|
}
|
||||||
|
|
||||||
|
soundtouch_device = SoundTouchDevice(name, remote_config)
|
||||||
|
DEVICES.append(soundtouch_device)
|
||||||
|
add_devices([soundtouch_device])
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE,
|
||||||
|
play_everywhere_service,
|
||||||
|
descriptions.get(SERVICE_PLAY_EVERYWHERE),
|
||||||
|
schema=SOUNDTOUCH_PLAY_EVERYWHERE)
|
||||||
|
hass.services.register(DOMAIN, SERVICE_CREATE_ZONE,
|
||||||
|
create_zone_service,
|
||||||
|
descriptions.get(SERVICE_CREATE_ZONE),
|
||||||
|
schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA)
|
||||||
|
hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE,
|
||||||
|
remove_zone_slave,
|
||||||
|
descriptions.get(SERVICE_REMOVE_ZONE_SLAVE),
|
||||||
|
schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA)
|
||||||
|
hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE,
|
||||||
|
add_zone_slave,
|
||||||
|
descriptions.get(SERVICE_ADD_ZONE_SLAVE),
|
||||||
|
schema=SOUNDTOUCH_ADD_ZONE_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def play_everywhere_service(service):
|
||||||
|
"""
|
||||||
|
Create a zone (multi-room) and play on all devices.
|
||||||
|
|
||||||
|
:param service: Home Assistant service with 'master' data set
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
- service: media_player.soundtouch_play_everywhere
|
||||||
|
data:
|
||||||
|
master: media_player.soundtouch_living_room
|
||||||
|
|
||||||
|
"""
|
||||||
|
master_device_id = service.data.get('master')
|
||||||
|
slaves = [d for d in DEVICES if d.entity_id != master_device_id]
|
||||||
|
master = next([device for device in DEVICES if
|
||||||
|
device.entity_id == master_device_id].__iter__(), None)
|
||||||
|
if master is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to find master with entity_id:" + str(master_device_id))
|
||||||
|
elif not slaves:
|
||||||
|
_LOGGER.warning("Unable to create zone without slaves")
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Creating zone with master " + str(master.device.config.name))
|
||||||
|
master.device.create_zone([slave.device for slave in slaves])
|
||||||
|
|
||||||
|
|
||||||
|
def create_zone_service(service):
|
||||||
|
"""
|
||||||
|
Create a zone (multi-room) on a master and play on specified slaves.
|
||||||
|
|
||||||
|
At least one master and one slave must be specified
|
||||||
|
|
||||||
|
:param service: Home Assistant service with 'master' and 'slaves' data set
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
- service: media_player.soundtouch_create_zone
|
||||||
|
data:
|
||||||
|
master: media_player.soundtouch_living_room
|
||||||
|
slaves:
|
||||||
|
- media_player.soundtouch_room
|
||||||
|
- media_player.soundtouch_kitchen
|
||||||
|
|
||||||
|
"""
|
||||||
|
master_device_id = service.data.get('master')
|
||||||
|
slaves_ids = service.data.get('slaves')
|
||||||
|
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
|
||||||
|
master = next([device for device in DEVICES if
|
||||||
|
device.entity_id == master_device_id].__iter__(), None)
|
||||||
|
if master is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to find master with entity_id:" + master_device_id)
|
||||||
|
elif not slaves:
|
||||||
|
_LOGGER.warning("Unable to create zone without slaves")
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Creating zone with master " + str(master.device.config.name))
|
||||||
|
master.device.create_zone([slave.device for slave in slaves])
|
||||||
|
|
||||||
|
|
||||||
|
def add_zone_slave(service):
|
||||||
|
"""
|
||||||
|
Add slave(s) to and existing zone (multi-room).
|
||||||
|
|
||||||
|
Zone must already exist and slaves array can not be empty.
|
||||||
|
|
||||||
|
:param service: Home Assistant service with 'master' and 'slaves' data set
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
- service: media_player.soundtouch_add_zone_slave
|
||||||
|
data:
|
||||||
|
master: media_player.soundtouch_living_room
|
||||||
|
slaves:
|
||||||
|
- media_player.soundtouch_room
|
||||||
|
|
||||||
|
"""
|
||||||
|
master_device_id = service.data.get('master')
|
||||||
|
slaves_ids = service.data.get('slaves')
|
||||||
|
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
|
||||||
|
master = next([device for device in DEVICES if
|
||||||
|
device.entity_id == master_device_id].__iter__(), None)
|
||||||
|
if master is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to find master with entity_id:" + str(master_device_id))
|
||||||
|
elif not slaves:
|
||||||
|
_LOGGER.warning("Unable to find slaves to add")
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Adding slaves to zone with master " + str(
|
||||||
|
master.device.config.name))
|
||||||
|
master.device.add_zone_slave([slave.device for slave in slaves])
|
||||||
|
|
||||||
|
|
||||||
|
def remove_zone_slave(service):
|
||||||
|
"""
|
||||||
|
Remove slave(s) from and existing zone (multi-room).
|
||||||
|
|
||||||
|
Zone must already exist and slaves array can not be empty.
|
||||||
|
Note: If removing last slave, the zone will be deleted and you'll have to
|
||||||
|
create a new one. You will not be able to add a new slave anymore
|
||||||
|
|
||||||
|
:param service: Home Assistant service with 'master' and 'slaves' data set
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
- service: media_player.soundtouch_remove_zone_slave
|
||||||
|
data:
|
||||||
|
master: media_player.soundtouch_living_room
|
||||||
|
slaves:
|
||||||
|
- media_player.soundtouch_room
|
||||||
|
|
||||||
|
"""
|
||||||
|
master_device_id = service.data.get('master')
|
||||||
|
slaves_ids = service.data.get('slaves')
|
||||||
|
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
|
||||||
|
master = next([device for device in DEVICES if
|
||||||
|
device.entity_id == master_device_id].__iter__(), None)
|
||||||
|
if master is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to find master with entity_id:" + master_device_id)
|
||||||
|
elif not slaves:
|
||||||
|
_LOGGER.warning("Unable to find slaves to remove")
|
||||||
|
else:
|
||||||
|
_LOGGER.info("Removing slaves from zone with master " +
|
||||||
|
str(master.device.config.name))
|
||||||
|
master.device.remove_zone_slave([slave.device for slave in slaves])
|
||||||
|
|
||||||
|
|
||||||
|
class SoundTouchDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a SoundTouch Bose device."""
|
||||||
|
|
||||||
|
def __init__(self, name, config):
|
||||||
|
"""Create Soundtouch Entity."""
|
||||||
|
from libsoundtouch import soundtouch_device
|
||||||
|
self._name = name
|
||||||
|
self._device = soundtouch_device(config['host'], config['port'])
|
||||||
|
self._status = self._device.status()
|
||||||
|
self._volume = self._device.volume()
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
"""Return specific soundtouch configuration."""
|
||||||
|
return self._config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""Return Soundtouch device."""
|
||||||
|
return self._device
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Retrieve the latest data."""
|
||||||
|
self._status = self._device.status()
|
||||||
|
self._volume = self._device.volume()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
return self._volume.actual / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
if self._status.source == 'STANDBY':
|
||||||
|
return STATE_OFF
|
||||||
|
else:
|
||||||
|
return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Boolean if volume is currently muted."""
|
||||||
|
return self._volume.muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_SOUNDTOUCH
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off media player."""
|
||||||
|
self._device.power_off()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn the media player on."""
|
||||||
|
self._device.power_on()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
def volume_up(self):
|
||||||
|
"""Volume up the media player."""
|
||||||
|
self._device.volume_up()
|
||||||
|
self._volume = self._device.volume()
|
||||||
|
|
||||||
|
def volume_down(self):
|
||||||
|
"""Volume down media player."""
|
||||||
|
self._device.volume_down()
|
||||||
|
self._volume = self._device.volume()
|
||||||
|
|
||||||
|
def set_volume_level(self, volume):
|
||||||
|
"""Set volume level, range 0..1."""
|
||||||
|
self._device.set_volume(int(volume * 100))
|
||||||
|
self._volume = self._device.volume()
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Send mute command."""
|
||||||
|
self._device.mute()
|
||||||
|
self._volume = self._device.volume()
|
||||||
|
|
||||||
|
def media_play_pause(self):
|
||||||
|
"""Simulate play pause media player."""
|
||||||
|
self._device.play_pause()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
"""Send play command."""
|
||||||
|
self._device.play()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
"""Send media pause command to media player."""
|
||||||
|
self._device.pause()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
"""Send next track command."""
|
||||||
|
self._device.next_track()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
"""Send the previous track command."""
|
||||||
|
self._device.previous_track()
|
||||||
|
self._status = self._device.status()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_image_url(self):
|
||||||
|
"""Image url of current playing media."""
|
||||||
|
return self._status.image
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_title(self):
|
||||||
|
"""Title of current playing media."""
|
||||||
|
if self._status.station_name is not None:
|
||||||
|
return self._status.station_name
|
||||||
|
elif self._status.artist is not None:
|
||||||
|
return self._status.artist + " - " + self._status.track
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_duration(self):
|
||||||
|
"""Duration of current playing media in seconds."""
|
||||||
|
return self._status.duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_artist(self):
|
||||||
|
"""Artist of current playing media."""
|
||||||
|
return self._status.artist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_track(self):
|
||||||
|
"""Artist of current playing media."""
|
||||||
|
return self._status.track
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_album_name(self):
|
||||||
|
"""Album name of current playing media."""
|
||||||
|
return self._status.album
|
||||||
|
|
||||||
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
|
"""Play a piece of media."""
|
||||||
|
_LOGGER.info("Starting media with media_id:" + str(media_id))
|
||||||
|
presets = self._device.presets()
|
||||||
|
preset = next([preset for preset in presets if
|
||||||
|
preset.preset_id == str(media_id)].__iter__(), None)
|
||||||
|
if preset is not None:
|
||||||
|
_LOGGER.info("Playing preset: " + preset.name)
|
||||||
|
self._device.select_preset(preset)
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Unable to find preset with id " + str(media_id))
|
|
@ -12,7 +12,7 @@ from homeassistant.components.mqtt import PROTOCOL_311
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
REQUIREMENTS = ['hbmqtt==0.7.1']
|
REQUIREMENTS = ['hbmqtt==0.8']
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,36 +10,110 @@ import socket
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE)
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.const import (CONF_STRUCTURE, CONF_FILENAME)
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
|
_CONFIGURING = {}
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['python-nest==2.11.0']
|
REQUIREMENTS = [
|
||||||
|
'http://github.com/technicalpickles/python-nest'
|
||||||
|
'/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip' # nest-cam branch
|
||||||
|
'#python-nest==3.0.0']
|
||||||
|
|
||||||
DOMAIN = 'nest'
|
DOMAIN = 'nest'
|
||||||
|
|
||||||
DATA_NEST = 'nest'
|
DATA_NEST = 'nest'
|
||||||
|
|
||||||
|
NEST_CONFIG_FILE = 'nest.conf'
|
||||||
|
CONF_CLIENT_ID = 'client_id'
|
||||||
|
CONF_CLIENT_SECRET = 'client_secret'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||||
vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string)
|
vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string)
|
||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def request_configuration(nest, hass, config):
|
||||||
|
"""Request configuration steps from the user."""
|
||||||
|
configurator = get_component('configurator')
|
||||||
|
if 'nest' in _CONFIGURING:
|
||||||
|
_LOGGER.debug("configurator failed")
|
||||||
|
configurator.notify_errors(
|
||||||
|
_CONFIGURING['nest'], "Failed to configure, please try again.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def nest_configuration_callback(data):
|
||||||
|
"""The actions to do when our configuration callback is called."""
|
||||||
|
_LOGGER.debug("configurator callback")
|
||||||
|
pin = data.get('pin')
|
||||||
|
setup_nest(hass, nest, config, pin=pin)
|
||||||
|
|
||||||
|
_CONFIGURING['nest'] = configurator.request_config(
|
||||||
|
hass, "Nest", nest_configuration_callback,
|
||||||
|
description=('To configure Nest, click Request Authorization below, '
|
||||||
|
'log into your Nest account, '
|
||||||
|
'and then enter the resulting PIN'),
|
||||||
|
link_name='Request Authorization',
|
||||||
|
link_url=nest.authorize_url,
|
||||||
|
submit_caption="Confirm",
|
||||||
|
fields=[{'id': 'pin', 'name': 'Enter the PIN', 'type': ''}]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_nest(hass, nest, config, pin=None):
|
||||||
|
"""Setup Nest Devices."""
|
||||||
|
if pin is not None:
|
||||||
|
_LOGGER.debug("pin acquired, requesting access token")
|
||||||
|
nest.request_token(pin)
|
||||||
|
|
||||||
|
if nest.access_token is None:
|
||||||
|
_LOGGER.debug("no access_token, requesting configuration")
|
||||||
|
request_configuration(nest, hass, config)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'nest' in _CONFIGURING:
|
||||||
|
_LOGGER.debug("configuration done")
|
||||||
|
configurator = get_component('configurator')
|
||||||
|
configurator.request_done(_CONFIGURING.pop('nest'))
|
||||||
|
|
||||||
|
_LOGGER.debug("proceeding with setup")
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
|
||||||
|
|
||||||
|
_LOGGER.debug("proceeding with discovery")
|
||||||
|
discovery.load_platform(hass, 'climate', DOMAIN, {}, config)
|
||||||
|
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||||
|
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||||
|
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
|
||||||
|
_LOGGER.debug("setup done")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the Nest thermostat component."""
|
"""Setup the Nest thermostat component."""
|
||||||
import nest
|
import nest
|
||||||
|
|
||||||
conf = config[DOMAIN]
|
if 'nest' in _CONFIGURING:
|
||||||
username = conf[CONF_USERNAME]
|
return
|
||||||
password = conf[CONF_PASSWORD]
|
|
||||||
|
|
||||||
nest = nest.Nest(username, password)
|
conf = config[DOMAIN]
|
||||||
hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
|
client_id = conf[CONF_CLIENT_ID]
|
||||||
|
client_secret = conf[CONF_CLIENT_SECRET]
|
||||||
|
filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
|
||||||
|
|
||||||
|
access_token_cache_file = hass.config.path(filename)
|
||||||
|
|
||||||
|
nest = nest.Nest(
|
||||||
|
access_token_cache_file=access_token_cache_file,
|
||||||
|
client_id=client_id, client_secret=client_secret)
|
||||||
|
setup_nest(hass, nest, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -85,3 +159,32 @@ class NestDevice(object):
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Connection error logging into the nest web service.")
|
"Connection error logging into the nest web service.")
|
||||||
|
|
||||||
|
def camera_devices(self):
|
||||||
|
"""Generator returning list of camera devices."""
|
||||||
|
try:
|
||||||
|
for structure in self.nest.structures:
|
||||||
|
if structure.name in self._structure:
|
||||||
|
for device in structure.cameradevices:
|
||||||
|
yield(structure, device)
|
||||||
|
else:
|
||||||
|
_LOGGER.info("Ignoring structure %s, not in %s",
|
||||||
|
structure.name, self._structure)
|
||||||
|
except socket.error:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Connection error logging into the nest web service.")
|
||||||
|
|
||||||
|
|
||||||
|
def is_thermostat(device):
|
||||||
|
"""Target devices that are Nest Thermostats."""
|
||||||
|
return bool(device.__class__.__name__ == 'Device')
|
||||||
|
|
||||||
|
|
||||||
|
def is_protect(device):
|
||||||
|
"""Target devices that are Nest Protect Smoke Alarms."""
|
||||||
|
return bool(device.__class__.__name__ == 'ProtectDevice')
|
||||||
|
|
||||||
|
|
||||||
|
def is_camera(device):
|
||||||
|
"""Target devices that are Nest Protect Smoke Alarms."""
|
||||||
|
return bool(device.__class__.__name__ == 'CameraDevice')
|
||||||
|
|
|
@ -4,35 +4,37 @@ Provides functionality to notify people.
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/notify/
|
https://home-assistant.io/components/notify/
|
||||||
"""
|
"""
|
||||||
from functools import partial
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.bootstrap as bootstrap
|
import homeassistant.bootstrap as bootstrap
|
||||||
from homeassistant.config import load_yaml_config_file
|
|
||||||
from homeassistant.helpers import config_per_platform
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||||
|
from homeassistant.helpers import config_per_platform
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
DOMAIN = "notify"
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Title of notification
|
|
||||||
ATTR_TITLE = "title"
|
|
||||||
ATTR_TITLE_DEFAULT = "Home Assistant"
|
|
||||||
|
|
||||||
# Target of the notification (user, device, etc)
|
|
||||||
ATTR_TARGET = 'target'
|
|
||||||
|
|
||||||
# Text to notify user of
|
|
||||||
ATTR_MESSAGE = "message"
|
|
||||||
|
|
||||||
# Platform specific data
|
# Platform specific data
|
||||||
ATTR_DATA = 'data'
|
ATTR_DATA = 'data'
|
||||||
|
|
||||||
SERVICE_NOTIFY = "notify"
|
# Text to notify user of
|
||||||
|
ATTR_MESSAGE = 'message'
|
||||||
|
|
||||||
|
# Target of the notification (user, device, etc)
|
||||||
|
ATTR_TARGET = 'target'
|
||||||
|
|
||||||
|
# Title of notification
|
||||||
|
ATTR_TITLE = 'title'
|
||||||
|
ATTR_TITLE_DEFAULT = "Home Assistant"
|
||||||
|
|
||||||
|
DOMAIN = 'notify'
|
||||||
|
|
||||||
|
SERVICE_NOTIFY = 'notify'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema({
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): cv.string,
|
vol.Required(CONF_PLATFORM): cv.string,
|
||||||
|
@ -46,8 +48,6 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_DATA): dict,
|
vol.Optional(ATTR_DATA): dict,
|
||||||
})
|
})
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def send_message(hass, message, title=None, data=None):
|
def send_message(hass, message, title=None, data=None):
|
||||||
"""Send a notification message."""
|
"""Send a notification message."""
|
||||||
|
@ -78,7 +78,7 @@ def setup(hass, config):
|
||||||
hass, config, DOMAIN, platform)
|
hass, config, DOMAIN, platform)
|
||||||
|
|
||||||
if notify_implementation is None:
|
if notify_implementation is None:
|
||||||
_LOGGER.error("Unknown notification service specified.")
|
_LOGGER.error("Unknown notification service specified")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
notify_service = notify_implementation.get_service(hass, p_config)
|
notify_service = notify_implementation.get_service(hass, p_config)
|
||||||
|
@ -114,7 +114,7 @@ def setup(hass, config):
|
||||||
if hasattr(notify_service, 'targets'):
|
if hasattr(notify_service, 'targets'):
|
||||||
platform_name = (p_config.get(CONF_NAME) or platform)
|
platform_name = (p_config.get(CONF_NAME) or platform)
|
||||||
for name, target in notify_service.targets.items():
|
for name, target in notify_service.targets.items():
|
||||||
target_name = slugify("{}_{}".format(platform_name, name))
|
target_name = slugify('{}_{}'.format(platform_name, name))
|
||||||
targets[target_name] = target
|
targets[target_name] = target
|
||||||
hass.services.register(DOMAIN, target_name,
|
hass.services.register(DOMAIN, target_name,
|
||||||
service_call_handler,
|
service_call_handler,
|
||||||
|
@ -124,10 +124,9 @@ def setup(hass, config):
|
||||||
platform_name = (p_config.get(CONF_NAME) or SERVICE_NOTIFY)
|
platform_name = (p_config.get(CONF_NAME) or SERVICE_NOTIFY)
|
||||||
platform_name_slug = slugify(platform_name)
|
platform_name_slug = slugify(platform_name)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, platform_name_slug,
|
hass.services.register(
|
||||||
service_call_handler,
|
DOMAIN, platform_name_slug, service_call_handler,
|
||||||
descriptions.get(SERVICE_NOTIFY),
|
descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA)
|
||||||
schema=NOTIFY_SERVICE_SCHEMA)
|
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['freesms==0.1.0']
|
REQUIREMENTS = ['freesms==0.1.1']
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
|
|
@ -107,8 +107,8 @@ def get_service(hass, config):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
hass.http.register_view(
|
hass.http.register_view(
|
||||||
HTML5PushRegistrationView(hass, registrations, json_path))
|
HTML5PushRegistrationView(registrations, json_path))
|
||||||
hass.http.register_view(HTML5PushCallbackView(hass, registrations))
|
hass.http.register_view(HTML5PushCallbackView(registrations))
|
||||||
|
|
||||||
gcm_api_key = config.get(ATTR_GCM_API_KEY)
|
gcm_api_key = config.get(ATTR_GCM_API_KEY)
|
||||||
gcm_sender_id = config.get(ATTR_GCM_SENDER_ID)
|
gcm_sender_id = config.get(ATTR_GCM_SENDER_ID)
|
||||||
|
@ -168,9 +168,8 @@ class HTML5PushRegistrationView(HomeAssistantView):
|
||||||
url = '/api/notify.html5'
|
url = '/api/notify.html5'
|
||||||
name = 'api:notify.html5'
|
name = 'api:notify.html5'
|
||||||
|
|
||||||
def __init__(self, hass, registrations, json_path):
|
def __init__(self, registrations, json_path):
|
||||||
"""Init HTML5PushRegistrationView."""
|
"""Init HTML5PushRegistrationView."""
|
||||||
super().__init__(hass)
|
|
||||||
self.registrations = registrations
|
self.registrations = registrations
|
||||||
self.json_path = json_path
|
self.json_path = json_path
|
||||||
|
|
||||||
|
@ -237,9 +236,8 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||||
url = '/api/notify.html5/callback'
|
url = '/api/notify.html5/callback'
|
||||||
name = 'api:notify.html5/callback'
|
name = 'api:notify.html5/callback'
|
||||||
|
|
||||||
def __init__(self, hass, registrations):
|
def __init__(self, registrations):
|
||||||
"""Init HTML5PushCallbackView."""
|
"""Init HTML5PushCallbackView."""
|
||||||
super().__init__(hass)
|
|
||||||
self.registrations = registrations
|
self.registrations = registrations
|
||||||
|
|
||||||
def decode_jwt(self, token):
|
def decode_jwt(self, token):
|
||||||
|
@ -324,7 +322,7 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||||
|
|
||||||
event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT,
|
event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT,
|
||||||
event_payload[ATTR_TYPE])
|
event_payload[ATTR_TYPE])
|
||||||
self.hass.bus.fire(event_name, event_payload)
|
request.app['hass'].bus.fire(event_name, event_payload)
|
||||||
return self.json({'status': 'ok',
|
return self.json({'status': 'ok',
|
||||||
'event': event_payload[ATTR_TYPE]})
|
'event': event_payload[ATTR_TYPE]})
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
||||||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['sendgrid==3.6.2']
|
REQUIREMENTS = ['sendgrid==3.6.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['slacker==0.9.29']
|
REQUIREMENTS = ['slacker==0.9.30']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
|
||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'--only-binary=all ' # avoid compilation of gattlib
|
'--only-binary=all ' # avoid compilation of gattlib
|
||||||
'git+https://github.com/getSenic/nuimo-linux-python'
|
'http://github.com/getSenic/nuimo-linux-python'
|
||||||
|
'/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip'
|
||||||
'#nuimo==1.0.0']
|
'#nuimo==1.0.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
|
@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
DOMAIN = 'recorder'
|
DOMAIN = 'recorder'
|
||||||
|
|
||||||
REQUIREMENTS = ['sqlalchemy==1.1.3']
|
REQUIREMENTS = ['sqlalchemy==1.1.4']
|
||||||
|
|
||||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||||
|
|
144
homeassistant/components/remote/__init__.py
Executable file
144
homeassistant/components/remote/__init__.py
Executable file
|
@ -0,0 +1,144 @@
|
||||||
|
"""
|
||||||
|
Component to interface with universal remote control devices.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/remote/
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||||
|
from homeassistant.components import group
|
||||||
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_ACTIVITY = 'activity'
|
||||||
|
ATTR_COMMAND = 'command'
|
||||||
|
ATTR_DEVICE = 'device'
|
||||||
|
|
||||||
|
DOMAIN = 'remote'
|
||||||
|
|
||||||
|
ENTITY_ID_ALL_REMOTES = group.ENTITY_ID_FORMAT.format('all_remotes')
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
|
GROUP_NAME_ALL_REMOTES = 'all remotes'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
|
SCAN_INTERVAL = 30
|
||||||
|
SERVICE_SEND_COMMAND = 'send_command'
|
||||||
|
SERVICE_SYNC = 'sync'
|
||||||
|
|
||||||
|
REMOTE_SERVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
REMOTE_SERVICE_TURN_ON_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Optional(ATTR_ACTIVITY): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_DEVICE): cv.string,
|
||||||
|
vol.Required(ATTR_COMMAND): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def is_on(hass, entity_id=None):
|
||||||
|
"""Return if the remote is on based on the statemachine."""
|
||||||
|
entity_id = entity_id or ENTITY_ID_ALL_REMOTES
|
||||||
|
return hass.states.is_state(entity_id, STATE_ON)
|
||||||
|
|
||||||
|
|
||||||
|
def turn_on(hass, activity=None, entity_id=None):
|
||||||
|
"""Turn all or specified remote on."""
|
||||||
|
data = {ATTR_ACTIVITY: activity}
|
||||||
|
if entity_id:
|
||||||
|
data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||||
|
|
||||||
|
|
||||||
|
def turn_off(hass, entity_id=None):
|
||||||
|
"""Turn all or specified remote off."""
|
||||||
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||||
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||||
|
|
||||||
|
|
||||||
|
def send_command(hass, device, command, entity_id=None):
|
||||||
|
"""Send a command to a device."""
|
||||||
|
data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
|
||||||
|
if entity_id:
|
||||||
|
data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(hass, entity_id=None):
|
||||||
|
"""Sync remote device."""
|
||||||
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||||
|
hass.services.call(DOMAIN, SERVICE_SYNC, data)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Track states and offer events for remotes."""
|
||||||
|
component = EntityComponent(
|
||||||
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES)
|
||||||
|
component.setup(config)
|
||||||
|
|
||||||
|
def handle_remote_service(service):
|
||||||
|
"""Handle calls to the remote services."""
|
||||||
|
target_remotes = component.extract_from_service(service)
|
||||||
|
|
||||||
|
activity_id = service.data.get(ATTR_ACTIVITY)
|
||||||
|
device = service.data.get(ATTR_DEVICE)
|
||||||
|
command = service.data.get(ATTR_COMMAND)
|
||||||
|
|
||||||
|
for remote in target_remotes:
|
||||||
|
if service.service == SERVICE_TURN_ON:
|
||||||
|
remote.turn_on(activity=activity_id)
|
||||||
|
elif service.service == SERVICE_SEND_COMMAND:
|
||||||
|
remote.send_command(device=device, command=command)
|
||||||
|
elif service.service == SERVICE_SYNC:
|
||||||
|
remote.sync()
|
||||||
|
else:
|
||||||
|
remote.turn_off()
|
||||||
|
|
||||||
|
if remote.should_poll:
|
||||||
|
remote.update_ha_state(True)
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_remote_service,
|
||||||
|
descriptions.get(SERVICE_TURN_OFF),
|
||||||
|
schema=REMOTE_SERVICE_SCHEMA)
|
||||||
|
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_remote_service,
|
||||||
|
descriptions.get(SERVICE_TURN_ON),
|
||||||
|
schema=REMOTE_SERVICE_TURN_ON_SCHEMA)
|
||||||
|
hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, handle_remote_service,
|
||||||
|
descriptions.get(SERVICE_SEND_COMMAND),
|
||||||
|
schema=REMOTE_SERVICE_SEND_COMMAND_SCHEMA)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteDevice(ToggleEntity):
|
||||||
|
"""Representation of a remote."""
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn a device on with the remote."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn a device off with the remote."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def send_command(self, **kwargs):
|
||||||
|
"""Send a command to a device."""
|
||||||
|
raise NotImplementedError()
|
57
homeassistant/components/remote/demo.py
Normal file
57
homeassistant/components/remote/demo.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
"""
|
||||||
|
Demo platform that has two fake remotes.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/demo/
|
||||||
|
"""
|
||||||
|
from homeassistant.components.remote import RemoteDevice
|
||||||
|
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
"""Setup the demo remotes."""
|
||||||
|
add_devices_callback([
|
||||||
|
DemoRemote('Remote One', False, None),
|
||||||
|
DemoRemote('Remote Two', True, 'mdi:remote'),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class DemoRemote(RemoteDevice):
|
||||||
|
"""Representation of a demo remote."""
|
||||||
|
|
||||||
|
def __init__(self, name, state, icon):
|
||||||
|
"""Initialize the Demo Remote."""
|
||||||
|
self._name = name or DEVICE_DEFAULT_NAME
|
||||||
|
self._state = state
|
||||||
|
self._icon = icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed for a demo remote."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device if any."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use for device if any."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if remote is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the remote on."""
|
||||||
|
self._state = True
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the remote off."""
|
||||||
|
self._state = False
|
||||||
|
self.schedule_update_ha_state()
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue