From ab6cb43d5b88eedf2f937652b15939625f024d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 13 Sep 2015 07:42:38 +0200 Subject: [PATCH 01/52] alarm component --- homeassistant/components/alarm/__init__.py | 106 +++++++++++++++++ homeassistant/components/alarm/verisure.py | 110 ++++++++++++++++++ .../www_static/home-assistant-polymer | 2 +- homeassistant/components/sensor/verisure.py | 28 ----- homeassistant/components/verisure.py | 8 +- homeassistant/const.py | 7 ++ homeassistant/loader.py | 2 + 7 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/alarm/__init__.py create mode 100644 homeassistant/components/alarm/verisure.py diff --git a/homeassistant/components/alarm/__init__.py b/homeassistant/components/alarm/__init__.py new file mode 100644 index 00000000000..449cbbbe452 --- /dev/null +++ b/homeassistant/components/alarm/__init__.py @@ -0,0 +1,106 @@ +""" +homeassistant.components.sensor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to interface with various sensors that can be monitored. +""" +import logging +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.components import verisure +from homeassistant.const import ( + STATE_UNKNOWN, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + ATTR_ENTITY_PICTURE, + SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) + +DOMAIN = 'alarm' +DEPENDENCIES = [] +SCAN_INTERVAL = 30 + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = { + verisure.DISCOVER_SENSORS: 'verisure' +} + +SERVICE_TO_METHOD = { + SERVICE_ALARM_DISARM: 'alarm_disarm', + SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', + SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', +} + +ATTR_CODE = 'code' + +ATTR_TO_PROPERTY = [ + ATTR_CODE, +] + +def setup(hass, config): + """ Track states and offer events for sensors. """ + component = EntityComponent( + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, + DISCOVERY_PLATFORMS) + + component.setup(config) + + def alarm_service_handler(service): + """ Maps services to methods on Alarm. """ + target_alarms = component.extract_from_service(service) + + if ATTR_CODE not in service.data: + return + + code = service.data[ATTR_CODE] + + method = SERVICE_TO_METHOD[service.service] + + for alarm in target_alarms: + getattr(alarm, method)(code) + + for service in SERVICE_TO_METHOD: + hass.services.register(DOMAIN, service, alarm_service_handler) + + return True + + +def alarm_disarm(hass, code, entity_id=None): + """ Send the alarm the command for disarm. """ + data = {ATTR_CODE: code} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) + + +def alarm_arm_home(hass, code, entity_id=None): + """ Send the alarm the command for arm home. """ + data = {ATTR_CODE: code} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) + +def alarm_arm_away(hass, code, entity_id=None): + """ Send the alarm the command for arm away. """ + data = {ATTR_CODE: code} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) + +class AlarmControl(Entity): + def alarm_disarm(self, code): + """ Send disar command. """ + raise NotImplementedError() + + def alarm_arm_home(self, code): + """ Send pause command. """ + raise NotImplementedError() + + def alarm_arm_away(self, code): + """ Send pause command. """ + raise NotImplementedError() diff --git a/homeassistant/components/alarm/verisure.py b/homeassistant/components/alarm/verisure.py new file mode 100644 index 00000000000..a8dbcec56f2 --- /dev/null +++ b/homeassistant/components/alarm/verisure.py @@ -0,0 +1,110 @@ +""" +homeassistant.components.alarm.verisure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Interfaces with Verisure alarm. +""" +import logging + +import homeassistant.components.verisure as verisure +import homeassistant.components.alarm as alarm + +from homeassistant.helpers.entity import Entity +<<<<<<< HEAD +from homeassistant.const import (STATE_UNKNOWN, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +======= +from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY +>>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Verisure platform. """ + + if not verisure.MY_PAGES: + _LOGGER.error('A connection has not been made to Verisure mypages.') + return False + +<<<<<<< HEAD + alarms = [] + + alarms.extend([ +======= + sensors = [] + + sensors.extend([ +>>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 + VerisureAlarm(value) + for value in verisure.get_alarm_status().values() + if verisure.SHOW_ALARM + ]) + +<<<<<<< HEAD + add_devices(alarms) + + +class VerisureAlarm(alarm.AlarmControl): +======= + add_devices(sensors) + + +class VerisureAlarm(Entity): +>>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 + """ represents a Verisure alarm status within home assistant. """ + + def __init__(self, alarm_status): + self._id = alarm_status.id + self._device = verisure.MY_PAGES.DEVICE_ALARM +<<<<<<< HEAD + self._state = STATE_UNKNOWN +======= +>>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 + + @property + def name(self): + """ Returns the name of the device. """ + return 'Alarm {}'.format(self._id) + + @property + def state(self): + """ Returns the state of the device. """ + if verisure.STATUS[self._device][self._id].status == 'unarmed': +<<<<<<< HEAD + self._state = STATE_ALARM_DISARMED + elif verisure.STATUS[self._device][self._id].status == 'armedhome': + self._state = STATE_ALARM_ARMED_HOME + elif verisure.STATUS[self._device][self._id].status == 'armedaway': + self._state = STATE_ALARM_ARMED_AWAY + elif verisure.STATUS[self._device][self._id].status != 'pending': + _LOGGER.error('Unknown alarm state ' + verisure.STATUS[self._device][self._id].status) + return self._state +======= + return STATE_ALARM_DISARMED + if verisure.STATUS[self._device][self._id].status == 'armed_home': + return STATE_ALARM_ARMED_HOME + if verisure.STATUS[self._device][self._id].status == 'armed_away': + return STATE_ALARM_ARMED_AWAY +>>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 + + def update(self): + ''' update alarm status ''' + verisure.update() +<<<<<<< HEAD + + def alarm_disarm(self, code): + """ Send disarm command. """ + verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_DISARMED) + _LOGGER.warning('disarming') + + def alarm_arm_home(self, code): + """ Send arm home command. """ + verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_ARMED_HOME) + _LOGGER.warning('arming home') + + def alarm_arm_away(self, code): + """ Send arm away command. """ + verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_ARMED_AWAY) + _LOGGER.warning('arming away') +======= +>>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 1c82a536312..b0b12e20e0f 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 1c82a536312e8321716ab7d80a5d17045d20d77f +Subproject commit b0b12e20e0f61df849c414c2dfbcf9923f784631 diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index 61af1089775..47efa197870 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -36,12 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hasattr(value, 'humidity') and value.humidity ]) - sensors.extend([ - VerisureAlarm(value) - for value in verisure.get_alarm_status().values() - if verisure.SHOW_ALARM - ]) - add_devices(sensors) @@ -103,25 +97,3 @@ class VerisureHygrometer(Entity): def update(self): ''' update sensor ''' verisure.update() - - -class VerisureAlarm(Entity): - """ represents a Verisure alarm status within home assistant. """ - - def __init__(self, alarm_status): - self._id = alarm_status.id - self._device = verisure.MY_PAGES.DEVICE_ALARM - - @property - def name(self): - """ Returns the name of the device. """ - return 'Alarm {}'.format(self._id) - - @property - def state(self): - """ Returns the state of the device. """ - return verisure.STATUS[self._device][self._id].label - - def update(self): - ''' update sensor ''' - verisure.update() diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index f084ce9874c..895a984cf86 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -58,8 +58,9 @@ from homeassistant.const import ( DOMAIN = "verisure" DISCOVER_SENSORS = 'verisure.sensors' DISCOVER_SWITCHES = 'verisure.switches' +DISCOVER_ALARMS = 'verisure.alarms' -DEPENDENCIES = [] +DEPENDENCIES = ['alarm'] REQUIREMENTS = [ 'https://github.com/persandstrom/python-verisure/archive/master.zip' ] @@ -121,7 +122,8 @@ def setup(hass, config): # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), - ('switch', DISCOVER_SWITCHES))): + ('switch', DISCOVER_SWITCHES), + ('alarm', DISCOVER_ALARMS))): component = get_component(comp_name) _LOGGER.info(config[DOMAIN]) bootstrap.setup_component(hass, component.DOMAIN, config) @@ -164,7 +166,7 @@ def reconnect(): def update(): ''' Updates the status of verisure components ''' if WRONG_PASSWORD_GIVEN: - # Is there any way to inform user? + _LOGGER.error('Wrong password') return try: diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d58dbb01d2..86c5a7ab6a5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -43,6 +43,9 @@ STATE_CLOSED = 'closed' STATE_PLAYING = 'playing' STATE_PAUSED = 'paused' STATE_IDLE = 'idle' +STATE_ALARM_DISARMED = 'disarmed' +STATE_ALARM_ARMED_HOME = 'armed_home' +STATE_ALARM_ARMED_AWAY = 'armed_away' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -110,6 +113,10 @@ SERVICE_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" SERVICE_MEDIA_SEEK = "media_seek" +SERVICE_ALARM_DISARM = "alarm_disarm" +SERVICE_ALARM_ARM_HOME = "alarm_arm_home" +SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" + # #### API / REMOTE #### SERVER_PORT = 8123 diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 7b755214252..356d20c0620 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -123,6 +123,8 @@ def get_component(comp_name): # custom_components/switch/some_platform.py exists, # the import custom_components.switch would succeeed. if module.__spec__.origin == 'namespace': + + print("!!!!!!!!!!!!!!!!!!!!! " + module.__spec__.origin) continue _LOGGER.info("Loaded %s from %s", comp_name, path) From c9bccadc404518047ca6a626fde6a0f17f52c2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 13 Sep 2015 07:48:34 +0200 Subject: [PATCH 02/52] fixed merge error --- homeassistant/components/alarm/verisure.py | 31 ---------------------- 1 file changed, 31 deletions(-) diff --git a/homeassistant/components/alarm/verisure.py b/homeassistant/components/alarm/verisure.py index a8dbcec56f2..1969273dfd1 100644 --- a/homeassistant/components/alarm/verisure.py +++ b/homeassistant/components/alarm/verisure.py @@ -9,12 +9,8 @@ import homeassistant.components.verisure as verisure import homeassistant.components.alarm as alarm from homeassistant.helpers.entity import Entity -<<<<<<< HEAD from homeassistant.const import (STATE_UNKNOWN, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) -======= -from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY ->>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 _LOGGER = logging.getLogger(__name__) @@ -26,40 +22,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error('A connection has not been made to Verisure mypages.') return False -<<<<<<< HEAD alarms = [] alarms.extend([ -======= - sensors = [] - - sensors.extend([ ->>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 VerisureAlarm(value) for value in verisure.get_alarm_status().values() if verisure.SHOW_ALARM ]) -<<<<<<< HEAD add_devices(alarms) class VerisureAlarm(alarm.AlarmControl): -======= - add_devices(sensors) - - -class VerisureAlarm(Entity): ->>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 """ represents a Verisure alarm status within home assistant. """ def __init__(self, alarm_status): self._id = alarm_status.id self._device = verisure.MY_PAGES.DEVICE_ALARM -<<<<<<< HEAD self._state = STATE_UNKNOWN -======= ->>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 @property def name(self): @@ -70,7 +50,6 @@ class VerisureAlarm(Entity): def state(self): """ Returns the state of the device. """ if verisure.STATUS[self._device][self._id].status == 'unarmed': -<<<<<<< HEAD self._state = STATE_ALARM_DISARMED elif verisure.STATUS[self._device][self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME @@ -79,18 +58,10 @@ class VerisureAlarm(Entity): elif verisure.STATUS[self._device][self._id].status != 'pending': _LOGGER.error('Unknown alarm state ' + verisure.STATUS[self._device][self._id].status) return self._state -======= - return STATE_ALARM_DISARMED - if verisure.STATUS[self._device][self._id].status == 'armed_home': - return STATE_ALARM_ARMED_HOME - if verisure.STATUS[self._device][self._id].status == 'armed_away': - return STATE_ALARM_ARMED_AWAY ->>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 def update(self): ''' update alarm status ''' verisure.update() -<<<<<<< HEAD def alarm_disarm(self, code): """ Send disarm command. """ @@ -106,5 +77,3 @@ class VerisureAlarm(Entity): """ Send arm away command. """ verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_ARMED_AWAY) _LOGGER.warning('arming away') -======= ->>>>>>> 614caa33ae4b9fd13bd26436dd4c1dd09ff01119 From 683a80f5f4a8fbd5de1278ea4b33077a54bb3b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 13 Sep 2015 20:21:02 +0200 Subject: [PATCH 03/52] tests pass --- .../__init__.py | 30 ++++++------- .../verisure.py | 42 +++++++++++-------- .../www_static/home-assistant-polymer | 2 +- homeassistant/components/verisure.py | 4 +- homeassistant/loader.py | 2 - 5 files changed, 44 insertions(+), 36 deletions(-) rename homeassistant/components/{alarm => alarm_control_panel}/__init__.py (88%) rename homeassistant/components/{alarm => alarm_control_panel}/verisure.py (66%) diff --git a/homeassistant/components/alarm/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py similarity index 88% rename from homeassistant/components/alarm/__init__.py rename to homeassistant/components/alarm_control_panel/__init__.py index 449cbbbe452..f31be44e64f 100644 --- a/homeassistant/components/alarm/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,5 +1,5 @@ """ -homeassistant.components.sensor +homeassistant.components.alarm_control_panel ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Component to interface with various sensors that can be monitored. """ @@ -8,12 +8,10 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import verisure from homeassistant.const import ( - STATE_UNKNOWN, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - ATTR_ENTITY_PICTURE, + ATTR_ENTITY_ID, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) -DOMAIN = 'alarm' +DOMAIN = 'alarm_control_panel' DEPENDENCIES = [] SCAN_INTERVAL = 30 @@ -30,12 +28,13 @@ SERVICE_TO_METHOD = { SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', } -ATTR_CODE = 'code' +ATTR_CODE = 'code' ATTR_TO_PROPERTY = [ ATTR_CODE, ] + def setup(hass, config): """ Track states and offer events for sensors. """ component = EntityComponent( @@ -43,15 +42,15 @@ def setup(hass, config): DISCOVERY_PLATFORMS) component.setup(config) - + def alarm_service_handler(service): """ Maps services to methods on Alarm. """ target_alarms = component.extract_from_service(service) - + if ATTR_CODE not in service.data: return - code = service.data[ATTR_CODE] + code = service.data[ATTR_CODE] method = SERVICE_TO_METHOD[service.service] @@ -72,7 +71,7 @@ def alarm_disarm(hass, code, entity_id=None): data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) - + def alarm_arm_home(hass, code, entity_id=None): """ Send the alarm the command for arm home. """ @@ -83,6 +82,7 @@ def alarm_arm_home(hass, code, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) + def alarm_arm_away(hass, code, entity_id=None): """ Send the alarm the command for arm away. """ data = {ATTR_CODE: code} @@ -92,15 +92,17 @@ def alarm_arm_away(hass, code, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) -class AlarmControl(Entity): + +class AlarmControlPanel(Entity): + """ ABC for alarm control devices. """ def alarm_disarm(self, code): - """ Send disar command. """ + """ Send disarm command. """ raise NotImplementedError() - + def alarm_arm_home(self, code): """ Send pause command. """ raise NotImplementedError() - + def alarm_arm_away(self, code): """ Send pause command. """ raise NotImplementedError() diff --git a/homeassistant/components/alarm/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py similarity index 66% rename from homeassistant/components/alarm/verisure.py rename to homeassistant/components/alarm_control_panel/verisure.py index 1969273dfd1..52d5a21a8b4 100644 --- a/homeassistant/components/alarm/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -1,16 +1,16 @@ """ -homeassistant.components.alarm.verisure +homeassistant.components.alarm_control_panel.verisure ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Interfaces with Verisure alarm. +Interfaces with Verisure alarm control panel. """ import logging import homeassistant.components.verisure as verisure -import homeassistant.components.alarm as alarm +import homeassistant.components.alarm_control_panel as alarm -from homeassistant.helpers.entity import Entity -from homeassistant.const import (STATE_UNKNOWN, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +from homeassistant.const import ( + STATE_UNKNOWN, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(alarms) -class VerisureAlarm(alarm.AlarmControl): +class VerisureAlarm(alarm.AlarmControlPanel): """ represents a Verisure alarm status within home assistant. """ def __init__(self, alarm_status): @@ -56,24 +56,32 @@ class VerisureAlarm(alarm.AlarmControl): elif verisure.STATUS[self._device][self._id].status == 'armedaway': self._state = STATE_ALARM_ARMED_AWAY elif verisure.STATUS[self._device][self._id].status != 'pending': - _LOGGER.error('Unknown alarm state ' + verisure.STATUS[self._device][self._id].status) + _LOGGER.error( + 'Unknown alarm state %s', + verisure.STATUS[self._device][self._id].status) return self._state def update(self): ''' update alarm status ''' verisure.update() - + def alarm_disarm(self, code): """ Send disarm command. """ - verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_DISARMED) - _LOGGER.warning('disarming') - + verisure.MY_PAGES.set_alarm_status( + code, + verisure.MY_PAGES.ALARM_DISARMED) + _LOGGER.warning('disarming') + def alarm_arm_home(self, code): """ Send arm home command. """ - verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_ARMED_HOME) - _LOGGER.warning('arming home') - + verisure.MY_PAGES.set_alarm_status( + code, + verisure.MY_PAGES.ALARM_ARMED_HOME) + _LOGGER.warning('arming home') + def alarm_arm_away(self, code): """ Send arm away command. """ - verisure.MY_PAGES.set_alarm_status(code, verisure.MY_PAGES.ALARM_ARMED_AWAY) - _LOGGER.warning('arming away') + verisure.MY_PAGES.set_alarm_status( + code, + verisure.MY_PAGES.ALARM_ARMED_AWAY) + _LOGGER.warning('arming away') diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index b0b12e20e0f..1c82a536312 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit b0b12e20e0f61df849c414c2dfbcf9923f784631 +Subproject commit 1c82a536312e8321716ab7d80a5d17045d20d77f diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 895a984cf86..4e97cca0cd4 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -58,9 +58,9 @@ from homeassistant.const import ( DOMAIN = "verisure" DISCOVER_SENSORS = 'verisure.sensors' DISCOVER_SWITCHES = 'verisure.switches' -DISCOVER_ALARMS = 'verisure.alarms' +DISCOVER_ALARMS = 'verisure.alarms_control_panel' -DEPENDENCIES = ['alarm'] +DEPENDENCIES = ['alarm_control_panel'] REQUIREMENTS = [ 'https://github.com/persandstrom/python-verisure/archive/master.zip' ] diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 356d20c0620..7b755214252 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -123,8 +123,6 @@ def get_component(comp_name): # custom_components/switch/some_platform.py exists, # the import custom_components.switch would succeeed. if module.__spec__.origin == 'namespace': - - print("!!!!!!!!!!!!!!!!!!!!! " + module.__spec__.origin) continue _LOGGER.info("Loaded %s from %s", comp_name, path) From 6c3a78df30973ff3038fdda9c230c0af31bef7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 13 Sep 2015 21:07:16 +0200 Subject: [PATCH 04/52] fixed spelling --- homeassistant/components/verisure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 5024efab6e6..3ae0ec9a7d9 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -59,7 +59,7 @@ from homeassistant.const import ( DOMAIN = "verisure" DISCOVER_SENSORS = 'verisure.sensors' DISCOVER_SWITCHES = 'verisure.switches' -DISCOVER_ALARMS = 'verisure.alarms_control_panel' +DISCOVER_ALARMS = 'verisure.alarm_control_panel' DEPENDENCIES = ['alarm_control_panel'] REQUIREMENTS = [ From 13ca42e18744fdb9151eafda38821f51f1faa952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Mon, 14 Sep 2015 17:33:43 +0200 Subject: [PATCH 05/52] fixes from review --- .../components/alarm_control_panel/__init__.py | 6 +++--- .../components/alarm_control_panel/verisure.py | 11 ++++++----- homeassistant/components/verisure.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index f31be44e64f..bf68e35ffe3 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,7 +1,7 @@ """ homeassistant.components.alarm_control_panel ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Component to interface with various sensors that can be monitored. +Component to interface with a alarm control panel. """ import logging from homeassistant.helpers.entity import Entity @@ -100,9 +100,9 @@ class AlarmControlPanel(Entity): raise NotImplementedError() def alarm_arm_home(self, code): - """ Send pause command. """ + """ Send arm home command. """ raise NotImplementedError() def alarm_arm_away(self, code): - """ Send pause command. """ + """ Send arm away command. """ raise NotImplementedError() diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 52d5a21a8b4..a317428ffcd 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -49,6 +49,12 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def state(self): """ Returns the state of the device. """ + return self._state + + def update(self): + ''' update alarm status ''' + verisure.update() + if verisure.STATUS[self._device][self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED elif verisure.STATUS[self._device][self._id].status == 'armedhome': @@ -59,11 +65,6 @@ class VerisureAlarm(alarm.AlarmControlPanel): _LOGGER.error( 'Unknown alarm state %s', verisure.STATUS[self._device][self._id].status) - return self._state - - def update(self): - ''' update alarm status ''' - verisure.update() def alarm_disarm(self, code): """ Send disarm command. """ diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 3ae0ec9a7d9..50fd9d6c7a9 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -125,7 +125,7 @@ def setup(hass, config): # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), ('switch', DISCOVER_SWITCHES), - ('alarm', DISCOVER_ALARMS))): + ('alarm_control_panel', DISCOVER_ALARMS))): component = get_component(comp_name) _LOGGER.info(config[DOMAIN]) bootstrap.setup_component(hass, component.DOMAIN, config) From f5d1da1d53b9cd386fb51efd0810d6a58c1f46fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Mon, 14 Sep 2015 19:42:36 +0200 Subject: [PATCH 06/52] and pylint... --- homeassistant/components/alarm_control_panel/verisure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index a317428ffcd..f19cdc102d2 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -54,7 +54,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): def update(self): ''' update alarm status ''' verisure.update() - + if verisure.STATUS[self._device][self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED elif verisure.STATUS[self._device][self._id].status == 'armedhome': From 2fe8b154f1ebe99b9920443a1674d07a5ec35a7e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2015 18:22:49 -0700 Subject: [PATCH 07/52] Fix state automation configuration --- homeassistant/components/automation/state.py | 7 +- tests/components/automation/test_state.py | 78 ++++++++++---------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index d336fcaa3d7..081026bb08b 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -14,6 +14,7 @@ CONF_ENTITY_ID = "state_entity_id" CONF_FROM = "state_from" CONF_TO = "state_to" CONF_STATE = "state" +CONF_IF_ENTITY_ID = "entity_id" def trigger(hass, config, action): @@ -40,13 +41,13 @@ def trigger(hass, config, action): def if_action(hass, config, action): """ Wraps action method with state based condition. """ - entity_id = config.get(CONF_ENTITY_ID) + entity_id = config.get(CONF_IF_ENTITY_ID) state = config.get(CONF_STATE) if entity_id is None or state is None: logging.getLogger(__name__).error( - "Missing if-condition configuration key %s or %s", CONF_ENTITY_ID, - CONF_STATE) + "Missing if-condition configuration key %s or %s", + CONF_IF_ENTITY_ID, CONF_STATE) return action def state_if(): diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 9dcfa49d54c..de8794b3337 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -8,8 +8,6 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation -from homeassistant.components.automation import event, state -from homeassistant.const import CONF_PLATFORM class TestAutomationState(unittest.TestCase): @@ -32,17 +30,17 @@ class TestAutomationState(unittest.TestCase): def test_setup_fails_if_no_entity_id(self): self.assertFalse(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'execute_service': 'test.automation' } })) def test_if_fires_on_entity_change(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.entity', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.entity', + 'execute_service': 'test.automation' } })) @@ -53,10 +51,10 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_from_filter(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.entity', - state.CONF_FROM: 'hello', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.entity', + 'state_from': 'hello', + 'execute_service': 'test.automation' } })) @@ -67,10 +65,10 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_to_filter(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.entity', - state.CONF_TO: 'world', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.entity', + 'state_to': 'world', + 'execute_service': 'test.automation' } })) @@ -81,11 +79,11 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_both_filters(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.entity', - state.CONF_FROM: 'hello', - state.CONF_TO: 'world', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.entity', + 'state_from': 'hello', + 'state_to': 'world', + 'execute_service': 'test.automation' } })) @@ -96,11 +94,11 @@ class TestAutomationState(unittest.TestCase): def test_if_not_fires_if_to_filter_not_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.entity', - state.CONF_FROM: 'hello', - state.CONF_TO: 'world', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.entity', + 'state_from': 'hello', + 'state_to': 'world', + 'execute_service': 'test.automation' } })) @@ -113,11 +111,11 @@ class TestAutomationState(unittest.TestCase): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.entity', - state.CONF_FROM: 'hello', - state.CONF_TO: 'world', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.entity', + 'state_from': 'hello', + 'state_to': 'world', + 'execute_service': 'test.automation' } })) @@ -128,9 +126,9 @@ class TestAutomationState(unittest.TestCase): def test_if_not_fires_if_entity_not_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: 'test.another_entity', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'state', + 'state_entity_id': 'test.another_entity', + 'execute_service': 'test.automation' } })) @@ -143,13 +141,13 @@ class TestAutomationState(unittest.TestCase): test_state = 'new_state' automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', - automation.CONF_IF: [{ - CONF_PLATFORM: 'state', - state.CONF_ENTITY_ID: entity_id, - state.CONF_STATE: test_state, + 'platform': 'event', + 'event_type': 'test_event', + 'execute_service': 'test.automation', + 'if': [{ + 'platform': 'state', + 'entity_id': entity_id, + 'state': test_state, }] } }) From fe2a9bb83e293612bbcf06ba62a7932e761cf14a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2015 20:46:57 -0700 Subject: [PATCH 08/52] Fix numeric state if --- homeassistant/components/automation/numeric_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 417ffffff7d..05a4f75a909 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -70,7 +70,7 @@ def if_action(hass, config, action): """ Execute action if state matches. """ state = hass.states.get(entity_id) - if state is None or _in_range(state.state, above, below): + if state is not None and _in_range(state.state, above, below): action() return state_if From 68c1dd7cd47c516df765a1742a62abca5c3d5f83 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2015 22:05:40 -0700 Subject: [PATCH 09/52] Refactor automation configuration --- .../components/automation/__init__.py | 125 ++++++--- homeassistant/components/automation/mqtt.py | 4 +- .../components/automation/numeric_state.py | 6 +- homeassistant/components/automation/state.py | 9 +- homeassistant/components/automation/time.py | 6 +- tests/components/automation/test_event.py | 77 ++++-- tests/components/automation/test_init.py | 134 ++++++++-- tests/components/automation/test_mqtt.py | 78 ++++-- .../automation/test_numeric_state.py | 186 +++++++------ tests/components/automation/test_state.py | 185 +++++++++++-- tests/components/automation/test_time.py | 249 +++++++++++++++++- 11 files changed, 836 insertions(+), 223 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a89afeb8c21..f659a2bdaff 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -7,7 +7,6 @@ Allows to setup simple automation rules via the config file. import logging from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.helpers import config_per_platform from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.components import logbook @@ -20,50 +19,45 @@ CONF_ALIAS = "alias" CONF_SERVICE = "execute_service" CONF_SERVICE_ENTITY_ID = "service_entity_id" CONF_SERVICE_DATA = "service_data" -CONF_IF = "if" + +CONF_CONDITION = "condition" +CONF_ACTION = 'action' +CONF_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Sets up automation. """ - success = False + config_key = DOMAIN + found = 1 - for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER): - platform = prepare_setup_platform(hass, config, DOMAIN, p_type) + while config_key in config: + p_config = _migrate_old_config(config[config_key]) + found += 1 + config_key = "{} {}".format(DOMAIN, found) - if platform is None: - _LOGGER.error("Unknown automation platform specified: %s", p_type) - continue - - action = _get_action(hass, p_config) + name = p_config.get(CONF_ALIAS, config_key) + action = _get_action(hass, p_config.get(CONF_ACTION, {}), name) if action is None: - return + continue - if CONF_IF in p_config: - action = _process_if(hass, config, p_config[CONF_IF], action) + if CONF_CONDITION in p_config: + action = _process_if(hass, config, p_config[CONF_CONDITION], action) - if platform.trigger(hass, p_config, action): - _LOGGER.info( - "Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, "")) - success = True - else: - _LOGGER.error( - "Error setting up rule %s", p_config.get(CONF_ALIAS, "")) + _process_trigger(hass, config, p_config.get(CONF_TRIGGER, []), name, + action) - return success + return True -def _get_action(hass, config): +def _get_action(hass, config, name): """ Return an action based on a config. """ - name = config.get(CONF_ALIAS, 'Unnamed automation') - if CONF_SERVICE not in config: - _LOGGER.error('Error setting up %s, no action specified.', - name) - return + _LOGGER.error('Error setting up %s, no action specified.', name) + return None def action(): """ Action to be executed. """ @@ -71,7 +65,6 @@ def _get_action(hass, config): logbook.log_entry(hass, name, 'has been triggered', DOMAIN) domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = config.get(CONF_SERVICE_DATA, {}) if not isinstance(service_data, dict): @@ -91,6 +84,37 @@ def _get_action(hass, config): return action +def _migrate_old_config(config): + """ Migrate old config to new. """ + if CONF_PLATFORM not in config: + return config + + _LOGGER.warning( + 'You are using an old configuration format. Please upgrade: ' + 'https://home-assistant.io/components/automation.html') + + new_conf = { + CONF_TRIGGER: dict(config), + CONF_CONDITION: config.get('if', []), + CONF_ACTION: dict(config), + } + + for cat, key, new_key in (('trigger', 'mqtt_topic', 'topic'), + ('trigger', 'mqtt_payload', 'payload'), + ('trigger', 'state_entity_id', 'entity_id'), + ('trigger', 'state_before', 'before'), + ('trigger', 'state_after', 'after'), + ('trigger', 'state_to', 'to'), + ('trigger', 'state_from', 'from'), + ('trigger', 'state_hours', 'hours'), + ('trigger', 'state_minutes', 'minutes'), + ('trigger', 'state_seconds', 'seconds')): + if key in new_conf[cat]: + new_conf[cat][new_key] = new_conf[cat].pop(key) + + return new_conf + + def _process_if(hass, config, if_configs, action): """ Processes if checks. """ @@ -98,19 +122,42 @@ def _process_if(hass, config, if_configs, action): if_configs = [if_configs] for if_config in if_configs: - p_type = if_config.get(CONF_PLATFORM) - if p_type is None: - _LOGGER.error("No platform defined found for if-statement %s", - if_config) - continue - - platform = prepare_setup_platform(hass, config, DOMAIN, p_type) - - if platform is None or not hasattr(platform, 'if_action'): - _LOGGER.error("Unsupported if-statement platform specified: %s", - p_type) + platform = _resolve_platform('condition', hass, config, + if_config.get(CONF_PLATFORM)) + if platform is None: continue action = platform.if_action(hass, if_config, action) return action + + +def _process_trigger(hass, config, trigger_configs, name, action): + """ Setup triggers. """ + if isinstance(trigger_configs, dict): + trigger_configs = [trigger_configs] + + for conf in trigger_configs: + platform = _resolve_platform('trigger', hass, config, + conf.get(CONF_PLATFORM)) + if platform is None: + continue + + if platform.trigger(hass, conf, action): + _LOGGER.info("Initialized rule %s", name) + else: + _LOGGER.error("Error setting up rule %s", name) + + +def _resolve_platform(requester, hass, config, platform): + """ Find automation platform. """ + if platform is None: + return None + platform = prepare_setup_platform(hass, config, DOMAIN, platform) + + if platform is None: + _LOGGER.error("Unknown automation platform specified for %s: %s", + requester, platform) + return None + + return platform diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 7004b919c72..3f85792f907 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -10,8 +10,8 @@ import homeassistant.components.mqtt as mqtt DEPENDENCIES = ['mqtt'] -CONF_TOPIC = 'mqtt_topic' -CONF_PAYLOAD = 'mqtt_payload' +CONF_TOPIC = 'topic' +CONF_PAYLOAD = 'payload' def trigger(hass, config, action): diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 05a4f75a909..95691d0ebcc 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -9,9 +9,9 @@ import logging from homeassistant.helpers.event import track_state_change -CONF_ENTITY_ID = "state_entity_id" -CONF_BELOW = "state_below" -CONF_ABOVE = "state_above" +CONF_ENTITY_ID = "entity_id" +CONF_BELOW = "below" +CONF_ABOVE = "above" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 081026bb08b..6f5a64e1070 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -10,11 +10,10 @@ from homeassistant.helpers.event import track_state_change from homeassistant.const import MATCH_ALL -CONF_ENTITY_ID = "state_entity_id" -CONF_FROM = "state_from" -CONF_TO = "state_to" +CONF_ENTITY_ID = "entity_id" +CONF_FROM = "from" +CONF_TO = "to" CONF_STATE = "state" -CONF_IF_ENTITY_ID = "entity_id" def trigger(hass, config, action): @@ -41,7 +40,7 @@ def trigger(hass, config, action): def if_action(hass, config, action): """ Wraps action method with state based condition. """ - entity_id = config.get(CONF_IF_ENTITY_ID) + entity_id = config.get(CONF_ENTITY_ID) state = config.get(CONF_STATE) if entity_id is None or state is None: diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index b97f3e2f7f5..b5bfcd274ee 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -10,9 +10,9 @@ from homeassistant.util import convert import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_time_change -CONF_HOURS = "time_hours" -CONF_MINUTES = "time_minutes" -CONF_SECONDS = "time_seconds" +CONF_HOURS = "hours" +CONF_MINUTES = "minutes" +CONF_SECONDS = "seconds" CONF_BEFORE = "before" CONF_AFTER = "after" CONF_WEEKDAY = "weekday" diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index a2c36283c9a..b0ea144ac49 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -8,8 +8,6 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation -import homeassistant.components.automation.event as event -from homeassistant.const import CONF_PLATFORM class TestAutomationEvent(unittest.TestCase): @@ -28,20 +26,57 @@ class TestAutomationEvent(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_fails_setup_if_no_event_type(self): - self.assertFalse(automation.setup(self.hass, { + def test_old_config_if_fires_on_event(self): + self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'event', + 'event_type': 'test_event', + 'execute_service': 'test.automation' } })) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_old_config_if_fires_on_event_with_data(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'event', + 'event_type': 'test_event', + 'event_data': {'some_attr': 'some_value'}, + 'execute_service': 'test.automation' + } + })) + + self.hass.bus.fire('test_event', {'some_attr': 'some_value'}) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_old_config_if_not_fires_if_event_data_not_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'event', + 'event_type': 'test_event', + 'event_data': {'some_attr': 'some_value'}, + 'execute_service': 'test.automation' + } + })) + + self.hass.bus.fire('test_event', {'some_attr': 'some_other_value'}) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + def test_if_fires_on_event(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'execute_service': 'test.automation', + } } })) @@ -52,10 +87,14 @@ class TestAutomationEvent(unittest.TestCase): def test_if_fires_on_event_with_data(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - event.CONF_EVENT_DATA: {'some_attr': 'some_value'}, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + 'event_data': {'some_attr': 'some_value'} + }, + 'action': { + 'execute_service': 'test.automation', + } } })) @@ -66,10 +105,14 @@ class TestAutomationEvent(unittest.TestCase): def test_if_not_fires_if_event_data_not_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - event.CONF_EVENT_DATA: {'some_attr': 'some_value'}, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + 'event_data': {'some_attr': 'some_value'} + }, + 'action': { + 'execute_service': 'test.automation', + } } })) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 507c37dc20a..e2477972ead 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -8,8 +8,7 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation -import homeassistant.components.automation.event as event -from homeassistant.const import CONF_PLATFORM, ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID class TestAutomationEvent(unittest.TestCase): @@ -28,20 +27,13 @@ class TestAutomationEvent(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_fails_if_unknown_platform(self): - self.assertFalse(automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'i_do_not_exist' - } - })) - def test_service_data_not_a_dict(self): automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', - automation.CONF_SERVICE_DATA: 100 + 'platform': 'event', + 'event_type': 'test_event', + 'execute_service': 'test.automation', + 'service_data': 100 } }) @@ -49,13 +41,64 @@ class TestAutomationEvent(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + def test_old_config_service_specify_data(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'event', + 'event_type': 'test_event', + 'execute_service': 'test.automation', + 'service_data': {'some': 'data'} + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('data', self.calls[0].data['some']) + + def test_old_config_service_specify_entity_id(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'event', + 'event_type': 'test_event', + 'execute_service': 'test.automation', + 'service_entity_id': 'hello.world' + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual(['hello.world'], + self.calls[0].data.get(ATTR_ENTITY_ID)) + + def test_old_config_service_specify_entity_id_list(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'event', + 'event_type': 'test_event', + 'execute_service': 'test.automation', + 'service_entity_id': ['hello.world', 'hello.world2'] + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual(['hello.world', 'hello.world2'], + self.calls[0].data.get(ATTR_ENTITY_ID)) + def test_service_specify_data(self): automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', - automation.CONF_SERVICE_DATA: {'some': 'data'} + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'execute_service': 'test.automation', + 'service_data': {'some': 'data'} + } } }) @@ -67,29 +110,66 @@ class TestAutomationEvent(unittest.TestCase): def test_service_specify_entity_id(self): automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', - automation.CONF_SERVICE_ENTITY_ID: 'hello.world' + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'execute_service': 'test.automation', + 'service_entity_id': 'hello.world' + } } }) self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - self.assertEqual(['hello.world'], self.calls[0].data[ATTR_ENTITY_ID]) + self.assertEqual(['hello.world'], + self.calls[0].data.get(ATTR_ENTITY_ID)) def test_service_specify_entity_id_list(self): automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', - automation.CONF_SERVICE_ENTITY_ID: ['hello.world', 'hello.world2'] + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'execute_service': 'test.automation', + 'service_entity_id': ['hello.world', 'hello.world2'] + } } }) self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - self.assertEqual(['hello.world', 'hello.world2'], self.calls[0].data[ATTR_ENTITY_ID]) + self.assertEqual(['hello.world', 'hello.world2'], + self.calls[0].data.get(ATTR_ENTITY_ID)) + + def test_two_triggers(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': [ + { + 'platform': 'event', + 'event_type': 'test_event', + }, + { + 'platform': 'state', + 'entity_id': 'test.entity', + } + ], + 'action': { + 'execute_service': 'test.automation', + 'service_entity_id': ['hello.world', 'hello.world2'] + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.hass.states.set('test.entity', 'hello') + self.hass.pool.block_till_done() + self.assertEqual(2, len(self.calls)) diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 9402b5300b6..eb6d6a51768 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -8,9 +8,6 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation -import homeassistant.components.automation.mqtt as mqtt -from homeassistant.const import CONF_PLATFORM - from tests.common import mock_mqtt_component, fire_mqtt_message @@ -31,20 +28,57 @@ class TestAutomationState(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_fails_if_no_topic(self): - self.assertFalse(automation.setup(self.hass, { + def test_old_config_if_fires_on_topic_match(self): + self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'mqtt', - automation.CONF_SERVICE: 'test.automation' + 'platform': 'mqtt', + 'mqtt_topic': 'test-topic', + 'execute_service': 'test.automation' } })) + fire_mqtt_message(self.hass, 'test-topic', '') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_old_config_if_fires_on_topic_and_payload_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'mqtt', + 'mqtt_topic': 'test-topic', + 'mqtt_payload': 'hello', + 'execute_service': 'test.automation' + } + })) + + fire_mqtt_message(self.hass, 'test-topic', 'hello') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_old_config_if_not_fires_on_topic_but_no_payload_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'platform': 'mqtt', + 'mqtt_topic': 'test-topic', + 'mqtt_payload': 'hello', + 'execute_service': 'test.automation' + } + })) + + fire_mqtt_message(self.hass, 'test-topic', 'no-hello') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + def test_if_fires_on_topic_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'mqtt', - mqtt.CONF_TOPIC: 'test-topic', - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'mqtt', + 'topic': 'test-topic' + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -55,10 +89,14 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_topic_and_payload_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'mqtt', - mqtt.CONF_TOPIC: 'test-topic', - mqtt.CONF_PAYLOAD: 'hello', - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'mqtt', + 'topic': 'test-topic', + 'payload': 'hello' + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -69,10 +107,14 @@ class TestAutomationState(unittest.TestCase): def test_if_not_fires_on_topic_but_no_payload_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'mqtt', - mqtt.CONF_TOPIC: 'test-topic', - mqtt.CONF_PAYLOAD: 'hello', - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'mqtt', + 'topic': 'test-topic', + 'payload': 'hello' + }, + 'action': { + 'execute_service': 'test.automation' + } } })) diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 19a0f183876..31987a67f2b 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -8,8 +8,6 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation -from homeassistant.components.automation import event, numeric_state -from homeassistant.const import CONF_PLATFORM class TestAutomationNumericState(unittest.TestCase): @@ -28,31 +26,17 @@ class TestAutomationNumericState(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_fails_if_no_entity_id(self): - self.assertFalse(automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' - } - })) - - def test_setup_fails_if_no_condition(self): - self.assertFalse(automation.setup(self.hass, { - automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - automation.CONF_SERVICE: 'test.automation' - } - })) - def test_if_fires_on_entity_change_below(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) # 9 is below 10 @@ -66,10 +50,14 @@ class TestAutomationNumericState(unittest.TestCase): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -78,17 +66,20 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_not_fires_on_entity_change_below_to_below(self): self.hass.states.set('test.entity', 9) self.hass.pool.block_till_done() self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -97,14 +88,17 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) - def test_if_fires_on_entity_change_above(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'above': 10, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) # 11 is above 10 @@ -119,10 +113,14 @@ class TestAutomationNumericState(unittest.TestCase): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'above': 10, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -131,7 +129,6 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_not_fires_on_entity_change_above_to_above(self): # set initial state self.hass.states.set('test.entity', 11) @@ -139,10 +136,14 @@ class TestAutomationNumericState(unittest.TestCase): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'above': 10, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -154,11 +155,15 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_below_range(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 5, - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + 'above': 5, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) # 9 is below 10 @@ -169,11 +174,15 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_below_above_range(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 5, - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + 'above': 5, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) # 4 is below 5 @@ -187,11 +196,15 @@ class TestAutomationNumericState(unittest.TestCase): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 5, - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + 'above': 5, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -206,11 +219,15 @@ class TestAutomationNumericState(unittest.TestCase): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.entity', - numeric_state.CONF_ABOVE: 5, - numeric_state.CONF_BELOW: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + 'above': 5, + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -222,10 +239,13 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_not_fires_if_entity_not_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: 'test.another_entity', - numeric_state.CONF_ABOVE: 10, - automation.CONF_SERVICE: 'test.automation' + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.another_entity', + }, + 'action': { + 'execute_service': 'test.automation' + } } })) @@ -238,19 +258,23 @@ class TestAutomationNumericState(unittest.TestCase): test_state = 10 automation.setup(self.hass, { automation.DOMAIN: { - CONF_PLATFORM: 'event', - event.CONF_EVENT_TYPE: 'test_event', - automation.CONF_SERVICE: 'test.automation', - automation.CONF_IF: [{ - CONF_PLATFORM: 'numeric_state', - numeric_state.CONF_ENTITY_ID: entity_id, - numeric_state.CONF_ABOVE: test_state, - numeric_state.CONF_BELOW: test_state + 2, - }] + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'numeric_state', + 'entity_id': entity_id, + 'above': test_state, + 'below': test_state + 2 + }, + 'action': { + 'execute_service': 'test.automation' + } } }) - self.hass.states.set(entity_id, test_state ) + self.hass.states.set(entity_id, test_state) self.hass.bus.fire('test_event') self.hass.pool.block_till_done() diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index de8794b3337..5b67c385f9c 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -27,15 +27,7 @@ class TestAutomationState(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_fails_if_no_entity_id(self): - self.assertFalse(automation.setup(self.hass, { - automation.DOMAIN: { - 'platform': 'state', - 'execute_service': 'test.automation' - } - })) - - def test_if_fires_on_entity_change(self): + def test_old_config_if_fires_on_entity_change(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'state', @@ -48,7 +40,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_fires_on_entity_change_with_from_filter(self): + def test_old_config_if_fires_on_entity_change_with_from_filter(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'state', @@ -62,7 +54,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_fires_on_entity_change_with_to_filter(self): + def test_old_config_if_fires_on_entity_change_with_to_filter(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'state', @@ -76,7 +68,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_fires_on_entity_change_with_both_filters(self): + def test_old_config_if_fires_on_entity_change_with_both_filters(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'state', @@ -91,7 +83,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_not_fires_if_to_filter_not_match(self): + def test_old_config_if_not_fires_if_to_filter_not_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'state', @@ -106,7 +98,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) - def test_if_not_fires_if_from_filter_not_match(self): + def test_old_config_if_not_fires_if_from_filter_not_match(self): self.hass.states.set('test.entity', 'bye') self.assertTrue(automation.setup(self.hass, { @@ -123,7 +115,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) - def test_if_not_fires_if_entity_not_match(self): + def test_old_config_if_not_fires_if_entity_not_match(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { 'platform': 'state', @@ -136,7 +128,7 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) - def test_if_action(self): + def test_old_config_if_action(self): entity_id = 'domain.test_entity' test_state = 'new_state' automation.setup(self.hass, { @@ -163,3 +155,164 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_with_from_filter(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'from': 'hello' + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_with_to_filter(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'to': 'world' + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_with_both_filters(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'from': 'hello', + 'to': 'world' + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_if_to_filter_not_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'from': 'hello', + 'to': 'world' + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'moon') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_if_from_filter_not_match(self): + self.hass.states.set('test.entity', 'bye') + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'from': 'hello', + 'to': 'world' + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_if_entity_not_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.anoter_entity', + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_action(self): + entity_id = 'domain.test_entity' + test_state = 'new_state' + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': [{ + 'platform': 'state', + 'entity_id': entity_id, + 'state': test_state + }], + 'action': { + 'execute_service': 'test.automation' + } + } + }) + + self.hass.states.set(entity_id, test_state) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, test_state + 'something') + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 48544bca25a..c135ba22223 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -33,7 +33,7 @@ class TestAutomationTime(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_if_fires_when_hour_matches(self): + def test_old_config_if_fires_when_hour_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'time', @@ -48,7 +48,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_fires_when_minute_matches(self): + def test_old_config_if_fires_when_minute_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'time', @@ -63,7 +63,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_fires_when_second_matches(self): + def test_old_config_if_fires_when_second_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'time', @@ -78,7 +78,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_fires_when_all_matches(self): + def test_old_config_if_fires_when_all_matches(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'time', @@ -96,13 +96,13 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_action_before(self): + def test_old_config_if_action_before(self): automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', automation.CONF_SERVICE: 'test.automation', - automation.CONF_IF: { + 'if': { CONF_PLATFORM: 'time', time.CONF_BEFORE: '10:00' } @@ -126,13 +126,13 @@ class TestAutomationTime(unittest.TestCase): self.assertEqual(1, len(self.calls)) - def test_if_action_after(self): + def test_old_config_if_action_after(self): automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', automation.CONF_SERVICE: 'test.automation', - automation.CONF_IF: { + 'if': { CONF_PLATFORM: 'time', time.CONF_AFTER: '10:00' } @@ -156,13 +156,13 @@ class TestAutomationTime(unittest.TestCase): self.assertEqual(1, len(self.calls)) - def test_if_action_one_weekday(self): + def test_old_config_if_action_one_weekday(self): automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', automation.CONF_SERVICE: 'test.automation', - automation.CONF_IF: { + 'if': { CONF_PLATFORM: 'time', time.CONF_WEEKDAY: 'mon', } @@ -187,13 +187,13 @@ class TestAutomationTime(unittest.TestCase): self.assertEqual(1, len(self.calls)) - def test_if_action_list_weekday(self): + def test_old_config_if_action_list_weekday(self): automation.setup(self.hass, { automation.DOMAIN: { CONF_PLATFORM: 'event', event.CONF_EVENT_TYPE: 'test_event', automation.CONF_SERVICE: 'test.automation', - automation.CONF_IF: { + 'if': { CONF_PLATFORM: 'time', time.CONF_WEEKDAY: ['mon', 'tue'], } @@ -225,3 +225,228 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(2, len(self.calls)) + + def test_if_fires_when_hour_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'hours': 0, + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_when_minute_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'minutes': 0, + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace(minute=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_when_second_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'seconds': 0, + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_when_all_matches(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace( + hour=0, minute=0, second=0)) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_before(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event' + }, + 'condition': { + 'platform': 'time', + 'before': '10:00', + }, + 'action': { + 'execute_service': 'test.automation' + } + } + }) + + before_10 = dt_util.now().replace(hour=8) + after_10 = dt_util.now().replace(hour=14) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=before_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=after_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_action_after(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event' + }, + 'condition': { + 'platform': 'time', + 'after': '10:00', + }, + 'action': { + 'execute_service': 'test.automation' + } + } + }) + + before_10 = dt_util.now().replace(hour=8) + after_10 = dt_util.now().replace(hour=14) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=before_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(0, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=after_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_action_one_weekday(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event' + }, + 'condition': { + 'platform': 'time', + 'weekday': 'mon', + }, + 'action': { + 'execute_service': 'test.automation' + } + } + }) + + days_past_monday = dt_util.now().weekday() + monday = dt_util.now() - timedelta(days=days_past_monday) + tuesday = monday + timedelta(days=1) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=monday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=tuesday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_action_list_weekday(self): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event' + }, + 'condition': { + 'platform': 'time', + 'weekday': ['mon', 'tue'], + }, + 'action': { + 'execute_service': 'test.automation' + } + } + }) + + days_past_monday = dt_util.now().weekday() + monday = dt_util.now() - timedelta(days=days_past_monday) + tuesday = monday + timedelta(days=1) + wednesday = tuesday + timedelta(days=1) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=monday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=tuesday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(2, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=wednesday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(2, len(self.calls)) From fc43135ddd7eb224d0e2dccf5bcd1ad48c9aeee6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2015 22:12:51 -0700 Subject: [PATCH 10/52] Style fix --- homeassistant/components/automation/state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 6f5a64e1070..7bd0542855c 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -45,8 +45,8 @@ def if_action(hass, config, action): if entity_id is None or state is None: logging.getLogger(__name__).error( - "Missing if-condition configuration key %s or %s", - CONF_IF_ENTITY_ID, CONF_STATE) + "Missing if-condition configuration key %s or %s", CONF_ENTITY_ID, + CONF_STATE) return action def state_if(): From 20f021d05f419bf233dc152349e0f5b31d080cfe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2015 22:14:15 -0700 Subject: [PATCH 11/52] Another style fix. Who comes up with this? --- homeassistant/components/automation/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f659a2bdaff..23126b2a3b3 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -44,7 +44,8 @@ def setup(hass, config): continue if CONF_CONDITION in p_config: - action = _process_if(hass, config, p_config[CONF_CONDITION], action) + action = _process_if(hass, config, p_config[CONF_CONDITION], + action) _process_trigger(hass, config, p_config.get(CONF_TRIGGER, []), name, action) From b2ad8db86bd67886f9bc4a049b49a5af5961f0cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2015 22:51:28 -0700 Subject: [PATCH 12/52] Add condition type to automation component --- .../components/automation/__init__.py | 33 ++++++- .../components/automation/numeric_state.py | 16 ++-- homeassistant/components/automation/state.py | 15 ++-- homeassistant/components/automation/time.py | 11 +-- tests/components/automation/test_init.py | 88 ++++++++++++++++++- 5 files changed, 137 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 23126b2a3b3..73411ddd1db 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -23,6 +23,11 @@ CONF_SERVICE_DATA = "service_data" CONF_CONDITION = "condition" CONF_ACTION = 'action' CONF_TRIGGER = "trigger" +CONF_CONDITION_TYPE = "condition_type" + +CONDITION_TYPE_AND = "and" +CONDITION_TYPE_OR = "or" +DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND _LOGGER = logging.getLogger(__name__) @@ -44,8 +49,13 @@ def setup(hass, config): continue if CONF_CONDITION in p_config: + cond_type = p_config.get(CONF_CONDITION_TYPE, + DEFAULT_CONDITION_TYPE).lower() action = _process_if(hass, config, p_config[CONF_CONDITION], - action) + action, cond_type) + + if action is None: + continue _process_trigger(hass, config, p_config.get(CONF_TRIGGER, []), name, action) @@ -116,21 +126,36 @@ def _migrate_old_config(config): return new_conf -def _process_if(hass, config, if_configs, action): +def _process_if(hass, config, if_configs, action, cond_type): """ Processes if checks. """ if isinstance(if_configs, dict): if_configs = [if_configs] + checks = [] for if_config in if_configs: platform = _resolve_platform('condition', hass, config, if_config.get(CONF_PLATFORM)) if platform is None: continue - action = platform.if_action(hass, if_config, action) + check = platform.if_action(hass, if_config) - return action + if check is None: + return None + + checks.append(check) + + if cond_type == CONDITION_TYPE_AND: + def if_action(): + if all(check() for check in checks): + action() + else: + def if_action(): + if any(check() for check in checks): + action() + + return if_action def _process_trigger(hass, config, trigger_configs, name, action): diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 95691d0ebcc..7e014213d62 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -48,14 +48,14 @@ def trigger(hass, config, action): return True -def if_action(hass, config, action): +def if_action(hass, config): """ Wraps action method with state based condition. """ entity_id = config.get(CONF_ENTITY_ID) if entity_id is None: _LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID) - return action + return None below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) @@ -64,16 +64,14 @@ def if_action(hass, config, action): _LOGGER.error("Missing configuration key." " One of %s or %s is required", CONF_BELOW, CONF_ABOVE) - return action - - def state_if(): - """ Execute action if state matches. """ + return None + def if_numeric_state(): + """ Test numeric state condition. """ state = hass.states.get(entity_id) - if state is not None and _in_range(state.state, above, below): - action() + return state is not None and _in_range(state.state, above, below) - return state_if + return if_numeric_state def _in_range(value, range_start, range_end): diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 7bd0542855c..bb936d36a1b 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -38,7 +38,7 @@ def trigger(hass, config, action): return True -def if_action(hass, config, action): +def if_action(hass, config): """ Wraps action method with state based condition. """ entity_id = config.get(CONF_ENTITY_ID) state = config.get(CONF_STATE) @@ -47,11 +47,12 @@ def if_action(hass, config, action): logging.getLogger(__name__).error( "Missing if-condition configuration key %s or %s", CONF_ENTITY_ID, CONF_STATE) - return action + return None - def state_if(): - """ Execute action if state matches. """ - if hass.states.is_state(entity_id, state): - action() + state = str(state) - return state_if + def if_state(): + """ Test if condition. """ + return hass.states.is_state(entity_id, state) + + return if_state diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index b5bfcd274ee..a7afa183ba0 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -36,7 +36,7 @@ def trigger(hass, config, action): return True -def if_action(hass, config, action): +def if_action(hass, config): """ Wraps action method with time based condition. """ before = config.get(CONF_BEFORE) after = config.get(CONF_AFTER) @@ -46,6 +46,7 @@ def if_action(hass, config, action): logging.getLogger(__name__).error( "Missing if-condition configuration key %s, %s or %s", CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY) + return None def time_if(): """ Validate time based if-condition """ @@ -59,7 +60,7 @@ def if_action(hass, config, action): minute=int(before_m)) if now > before_point: - return + return False if after is not None: # Strip seconds if given @@ -68,15 +69,15 @@ def if_action(hass, config, action): after_point = now.replace(hour=int(after_h), minute=int(after_m)) if now < after_point: - return + return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] if isinstance(weekday, str) and weekday != now_weekday or \ now_weekday not in weekday: - return + return False - action() + return True return time_if diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index e2477972ead..8553a4472be 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -162,7 +162,6 @@ class TestAutomationEvent(unittest.TestCase): ], 'action': { 'execute_service': 'test.automation', - 'service_entity_id': ['hello.world', 'hello.world2'] } } }) @@ -173,3 +172,90 @@ class TestAutomationEvent(unittest.TestCase): self.hass.states.set('test.entity', 'hello') self.hass.pool.block_till_done() self.assertEqual(2, len(self.calls)) + + def test_two_conditions_with_and(self): + entity_id = 'test.entity' + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': [ + { + 'platform': 'event', + 'event_type': 'test_event', + }, + ], + 'condition': [ + { + 'platform': 'state', + 'entity_id': entity_id, + 'state': 100 + }, + { + 'platform': 'numeric_state', + 'entity_id': entity_id, + 'below': 150 + } + ], + 'action': { + 'execute_service': 'test.automation', + } + } + }) + + self.hass.states.set(entity_id, 100) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, 101) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, 151) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_two_conditions_with_or(self): + entity_id = 'test.entity' + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': [ + { + 'platform': 'event', + 'event_type': 'test_event', + }, + ], + 'condition_type': 'OR', + 'condition': [ + { + 'platform': 'state', + 'entity_id': entity_id, + 'state': 200 + }, + { + 'platform': 'numeric_state', + 'entity_id': entity_id, + 'below': 150 + } + ], + 'action': { + 'execute_service': 'test.automation', + } + } + }) + + self.hass.states.set(entity_id, 200) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, 100) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(2, len(self.calls)) + + self.hass.states.set(entity_id, 250) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(2, len(self.calls)) From e26f0f7b7d98ce53fa1e850270b89c446b96089c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 00:02:46 -0700 Subject: [PATCH 13/52] Update stale header doc --- tests/components/automation/test_event.py | 6 +++--- tests/components/automation/test_init.py | 7 +++---- tests/components/automation/test_mqtt.py | 6 +++--- tests/components/automation/test_numeric_state.py | 6 +++--- tests/components/automation/test_state.py | 6 +++--- tests/components/automation/test_time.py | 6 +++--- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index b0ea144ac49..01867f3850e 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -1,8 +1,8 @@ """ -tests.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +tests.components.automation.test_event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests demo component. +Tests event automation. """ import unittest diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 8553a4472be..df8b199b700 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,8 +1,7 @@ """ -tests.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tests demo component. +tests.components.automation.test_init +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Tests automation component. """ import unittest diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index eb6d6a51768..d5e969abe5d 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -1,8 +1,8 @@ """ -tests.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +tests.components.automation.test_mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests demo component. +Tests mqtt automation. """ import unittest diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 31987a67f2b..e946c138a95 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,8 +1,8 @@ """ -tests.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +tests.components.automation.test_numeric_state +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests demo component. +Tests numeric state automation. """ import unittest diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 5b67c385f9c..991c3e066d4 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -1,8 +1,8 @@ """ -tests.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +tests.components.automation.test_state +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests demo component. +Tests state automation. """ import unittest diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index c135ba22223..96579af9aa8 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -1,8 +1,8 @@ """ -tests.test_component_demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +tests.components.automation.test_time +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests demo component. +Tests time automation. """ from datetime import timedelta import unittest From 2978e0dabebfea1509eb7f8d678d3f50ceaf6139 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 00:02:54 -0700 Subject: [PATCH 14/52] Add sun automation trigger --- homeassistant/components/automation/sun.py | 103 +++++++++++++++++ tests/components/automation/test_sun.py | 128 +++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 homeassistant/components/automation/sun.py create mode 100644 tests/components/automation/test_sun.py diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py new file mode 100644 index 00000000000..103df6c9b39 --- /dev/null +++ b/homeassistant/components/automation/sun.py @@ -0,0 +1,103 @@ +""" +homeassistant.components.automation.sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Offers sun based automation rules. +""" +import logging +from datetime import timedelta + +from homeassistant.components import sun +from homeassistant.helpers.event import track_point_in_utc_time +import homeassistant.util.dt as dt_util + +DEPENDENCIES = ['sun'] + +CONF_OFFSET = 'offset' +CONF_EVENT = 'event' + +EVENT_SUNSET = 'sunset' +EVENT_SUNRISE = 'sunrise' + +_LOGGER = logging.getLogger(__name__) + + +def trigger(hass, config, action): + """ Listen for events based on config. """ + event = config.get(CONF_EVENT) + + if event is None: + _LOGGER.error("Missing configuration key %s", CONF_EVENT) + return False + + event = event.lower() + if event not in (EVENT_SUNRISE, EVENT_SUNSET): + _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) + return False + + if CONF_OFFSET in config: + raw_offset = config.get(CONF_OFFSET) + + negative_offset = False + if raw_offset.startswith('-'): + negative_offset = True + raw_offset = raw_offset[1:] + + try: + (hour, minute, second) = [int(x) for x in raw_offset.split(':')] + except ValueError: + _LOGGER.error('Could not parse offset %s', raw_offset) + return False + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if negative_offset: + offset *= -1 + else: + offset = timedelta(0) + + # Do something to call action + if event == EVENT_SUNRISE: + trigger_sunrise(hass, action, offset) + else: + trigger_sunset(hass, action, offset) + + return True + + +def trigger_sunrise(hass, action, offset): + """ Trigger action at next sun rise. """ + def next_rise(): + """ Returns next sunrise. """ + next_time = sun.next_rising_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunrise_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + action() + + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + + +def trigger_sunset(hass, action, offset): + """ Trigger action at next sun set. """ + def next_set(): + """ Returns next sunrise. """ + next_time = sun.next_setting_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunset_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + action() + + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py new file mode 100644 index 00000000000..c2b292ea0e5 --- /dev/null +++ b/tests/components/automation/test_sun.py @@ -0,0 +1,128 @@ +""" +tests.components.automation.test_sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests sun automation. +""" +from datetime import datetime +import unittest + +import homeassistant.core as ha +from homeassistant.components import sun +import homeassistant.components.automation as automation +import homeassistant.util.dt as dt_util + +from tests.common import fire_time_changed + + +class TestAutomationSun(unittest.TestCase): + """ Test the sun automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.hass.config.components.append('sun') + + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_sunset_trigger(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '02:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_sunrise_trigger(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_sunset_trigger_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '02:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + 'offset': '0:30:00' + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_sunrise_trigger_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + 'offset': '-0:30:00' + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) From 1ec5178f66e3491d9eec705877a1c8805acecceb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 00:05:20 -0700 Subject: [PATCH 15/52] Remove scheduler component --- .../components/scheduler/__init__.py | 137 ------------------ homeassistant/components/scheduler/time.py | 70 --------- homeassistant/components/sun.py | 96 +----------- 3 files changed, 1 insertion(+), 302 deletions(-) delete mode 100644 homeassistant/components/scheduler/__init__.py delete mode 100644 homeassistant/components/scheduler/time.py diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py deleted file mode 100644 index 1a67636da3d..00000000000 --- a/homeassistant/components/scheduler/__init__.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -homeassistant.components.scheduler -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A component that will act as a scheduler and perform actions based -on the events in the schedule. - -It will read a json object from schedule.json in the config dir -and create a schedule based on it. -Each schedule is a JSON with the keys id, name, description, -entity_ids, and events. -- days is an array with the weekday number (monday=0) that the schedule - is active -- entity_ids an array with entity ids that the events in the schedule should - effect (can also be groups) -- events is an array of objects that describe the different events that is - supported. Read in the events descriptions for more information. -""" -import logging -import json - -from homeassistant import bootstrap -from homeassistant.loader import get_component -from homeassistant.const import ATTR_ENTITY_ID - -DOMAIN = 'scheduler' - -DEPENDENCIES = [] - -_LOGGER = logging.getLogger(__name__) - -_SCHEDULE_FILE = 'schedule.json' - - -def setup(hass, config): - """ Create the schedules. """ - - def setup_listener(schedule, event_data): - """ Creates the event listener based on event_data. """ - event_type = event_data['type'] - component = event_type - - # if the event isn't part of a component - if event_type in ['time']: - component = 'scheduler.{}'.format(event_type) - - elif not bootstrap.setup_component(hass, component, config): - _LOGGER.warn("Could setup event listener for %s", component) - return None - - return get_component(component).create_event_listener(schedule, - event_data) - - def setup_schedule(schedule_data): - """ Setup a schedule based on the description. """ - - schedule = Schedule(schedule_data['id'], - name=schedule_data['name'], - description=schedule_data['description'], - entity_ids=schedule_data['entity_ids'], - days=schedule_data['days']) - - for event_data in schedule_data['events']: - event_listener = setup_listener(schedule, event_data) - - if event_listener: - schedule.add_event_listener(event_listener) - - schedule.schedule(hass) - return True - - with open(hass.config.path(_SCHEDULE_FILE)) as schedule_file: - schedule_descriptions = json.load(schedule_file) - - for schedule_description in schedule_descriptions: - if not setup_schedule(schedule_description): - return False - - return True - - -class Schedule(object): - """ A Schedule """ - - # pylint: disable=too-many-arguments - def __init__(self, schedule_id, name=None, description=None, - entity_ids=None, days=None): - - self.schedule_id = schedule_id - self.name = name - self.description = description - - self.entity_ids = entity_ids or [] - - self.days = days or [0, 1, 2, 3, 4, 5, 6] - - self.__event_listeners = [] - - def add_event_listener(self, event_listener): - """ Add a event to the schedule. """ - self.__event_listeners.append(event_listener) - - def schedule(self, hass): - """ Schedule all the events in the schedule. """ - for event in self.__event_listeners: - event.schedule(hass) - - -class EventListener(object): - """ The base EventListener class that the schedule uses. """ - def __init__(self, schedule): - self.my_schedule = schedule - - def schedule(self, hass): - """ Schedule the event """ - pass - - def execute(self, hass): - """ execute the event """ - pass - - -# pylint: disable=too-few-public-methods -class ServiceEventListener(EventListener): - """ A EventListener that calls a service when executed. """ - - def __init__(self, schdule, service): - EventListener.__init__(self, schdule) - - (self.domain, self.service) = service.split('.') - - def execute(self, hass): - """ Call the service. """ - data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids} - hass.services.call(self.domain, self.service, data) - - # Reschedule for next day - self.schedule(hass) diff --git a/homeassistant/components/scheduler/time.py b/homeassistant/components/scheduler/time.py deleted file mode 100644 index 4d0280dfdf9..00000000000 --- a/homeassistant/components/scheduler/time.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -homeassistant.components.scheduler.time -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An event in the scheduler component that will call the service -every specified day at the time specified. -A time event need to have the type 'time', which service to call and at -which time. - -{ - "type": "time", - "service": "switch.turn_off", - "time": "22:00:00" -} - -""" -from datetime import timedelta -import logging - -import homeassistant.util.dt as dt_util -from homeassistant.helpers.event import track_point_in_time -from homeassistant.components.scheduler import ServiceEventListener - -_LOGGER = logging.getLogger(__name__) - - -def create_event_listener(schedule, event_listener_data): - """ Create a TimeEvent based on the description. """ - - service = event_listener_data['service'] - (hour, minute, second) = [int(x) for x in - event_listener_data['time'].split(':', 3)] - - return TimeEventListener(schedule, service, hour, minute, second) - - -# pylint: disable=too-few-public-methods -class TimeEventListener(ServiceEventListener): - """ The time event that the scheduler uses. """ - - # pylint: disable=too-many-arguments - def __init__(self, schedule, service, hour, minute, second): - ServiceEventListener.__init__(self, schedule, service) - - self.hour = hour - self.minute = minute - self.second = second - - def schedule(self, hass): - """ Schedule this event so that it will be called. """ - - next_time = dt_util.now().replace( - hour=self.hour, minute=self.minute, second=self.second) - - # Calculate the next time the event should be executed. - # That is the next day that the schedule is configured to run - while next_time < dt_util.now() or \ - next_time.weekday() not in self.my_schedule.days: - - next_time = next_time + timedelta(days=1) - - # pylint: disable=unused-argument - def execute(now): - """ Call the execute method """ - self.execute(hass) - - track_point_in_time(hass, execute, next_time) - - _LOGGER.info( - 'TimeEventListener scheduled for %s, will call service %s.%s', - next_time, self.domain, self.service) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 802eddb4a3a..ce4dbd1e937 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -25,10 +25,8 @@ import urllib import homeassistant.util as util import homeassistant.util.dt as dt_util -from homeassistant.helpers.event import ( - track_point_in_utc_time, track_point_in_time) +from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.entity import Entity -from homeassistant.components.scheduler import ServiceEventListener DEPENDENCIES = [] REQUIREMENTS = ['astral==0.8.1'] @@ -214,95 +212,3 @@ class Sun(Entity): track_point_in_utc_time( self.hass, self.point_in_time_listener, self.next_change + timedelta(seconds=1)) - - -def create_event_listener(schedule, event_listener_data): - """ Create a sun event listener based on the description. """ - - negative_offset = False - service = event_listener_data['service'] - offset_str = event_listener_data['offset'] - event = event_listener_data['event'] - - if offset_str.startswith('-'): - negative_offset = True - offset_str = offset_str[1:] - - (hour, minute, second) = [int(x) for x in offset_str.split(':')] - - offset = timedelta(hours=hour, minutes=minute, seconds=second) - - if event == 'sunset': - return SunsetEventListener(schedule, service, offset, negative_offset) - - return SunriseEventListener(schedule, service, offset, negative_offset) - - -# pylint: disable=too-few-public-methods -class SunEventListener(ServiceEventListener): - """ This is the base class for sun event listeners. """ - - def __init__(self, schedule, service, offset, negative_offset): - ServiceEventListener.__init__(self, schedule, service) - - self.offset = offset - self.negative_offset = negative_offset - - def __get_next_time(self, next_event): - """ - Returns when the next time the service should be called. - Taking into account the offset and which days the event should execute. - """ - - if self.negative_offset: - next_time = next_event - self.offset - else: - next_time = next_event + self.offset - - while next_time < dt_util.now() or \ - next_time.weekday() not in self.my_schedule.days: - next_time = next_time + timedelta(days=1) - - return next_time - - def schedule_next_event(self, hass, next_event): - """ Schedule the event. """ - next_time = self.__get_next_time(next_event) - - # pylint: disable=unused-argument - def execute(now): - """ Call the execute method. """ - self.execute(hass) - - track_point_in_time(hass, execute, next_time) - - return next_time - - -# pylint: disable=too-few-public-methods -class SunsetEventListener(SunEventListener): - """ This class is used the call a service when the sun sets. """ - def schedule(self, hass): - """ Schedule the event """ - next_setting_dt = next_setting(hass) - - next_time_dt = self.schedule_next_event(hass, next_setting_dt) - - _LOGGER.info( - 'SunsetEventListener scheduled for %s, will call service %s.%s', - next_time_dt, self.domain, self.service) - - -# pylint: disable=too-few-public-methods -class SunriseEventListener(SunEventListener): - """ This class is used the call a service when the sun rises. """ - - def schedule(self, hass): - """ Schedule the event. """ - next_rising_dt = next_rising(hass) - - next_time_dt = self.schedule_next_event(hass, next_rising_dt) - - _LOGGER.info( - 'SunriseEventListener scheduled for %s, will call service %s.%s', - next_time_dt, self.domain, self.service) From ae527e9c6f60ba5960f0216d35d63ae691ed11c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 00:07:49 -0700 Subject: [PATCH 16/52] Fix broken sun automation test --- tests/components/automation/test_sun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index c2b292ea0e5..dcb9cbafc1f 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -67,7 +67,7 @@ class TestAutomationSun(unittest.TestCase): automation.DOMAIN: { 'trigger': { 'platform': 'sun', - 'event': 'sunset', + 'event': 'sunrise', }, 'action': { 'execute_service': 'test.automation', @@ -114,7 +114,7 @@ class TestAutomationSun(unittest.TestCase): automation.DOMAIN: { 'trigger': { 'platform': 'sun', - 'event': 'sunset', + 'event': 'sunrise', 'offset': '-0:30:00' }, 'action': { From 0584c10ef96f5a23d2b6d9ae34a6b6dc3b8ae1a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 00:11:24 -0700 Subject: [PATCH 17/52] Style fix --- homeassistant/components/automation/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 73411ddd1db..0bb47a97a3c 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -148,10 +148,12 @@ def _process_if(hass, config, if_configs, action, cond_type): if cond_type == CONDITION_TYPE_AND: def if_action(): + """ AND all conditions. """ if all(check() for check in checks): action() else: def if_action(): + """ OR all conditions. """ if any(check() for check in checks): action() From c18294ee76ac6a6af233558acebeb8fc44c09b72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 08:56:06 -0700 Subject: [PATCH 18/52] Allow triggers to be used as condition --- .../components/automation/__init__.py | 53 ++++++++------- homeassistant/components/automation/state.py | 2 +- homeassistant/components/automation/time.py | 24 ++++--- homeassistant/util/dt.py | 17 +++++ tests/components/automation/test_init.py | 64 +++++++++++++++++++ tests/components/automation/test_state.py | 18 ++++++ tests/components/automation/test_time.py | 23 +++++-- 7 files changed, 166 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 0bb47a97a3c..45859617624 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,22 +11,24 @@ from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.components import logbook -DOMAIN = "automation" +DOMAIN = 'automation' -DEPENDENCIES = ["group"] +DEPENDENCIES = ['group'] -CONF_ALIAS = "alias" -CONF_SERVICE = "execute_service" -CONF_SERVICE_ENTITY_ID = "service_entity_id" -CONF_SERVICE_DATA = "service_data" +CONF_ALIAS = 'alias' +CONF_SERVICE = 'execute_service' +CONF_SERVICE_ENTITY_ID = 'service_entity_id' +CONF_SERVICE_DATA = 'service_data' -CONF_CONDITION = "condition" +CONF_CONDITION = 'condition' CONF_ACTION = 'action' -CONF_TRIGGER = "trigger" -CONF_CONDITION_TYPE = "condition_type" +CONF_TRIGGER = 'trigger' +CONF_CONDITION_TYPE = 'condition_type' + +CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values' +CONDITION_TYPE_AND = 'and' +CONDITION_TYPE_OR = 'or' -CONDITION_TYPE_AND = "and" -CONDITION_TYPE_OR = "or" DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND _LOGGER = logging.getLogger(__name__) @@ -48,11 +50,8 @@ def setup(hass, config): if action is None: continue - if CONF_CONDITION in p_config: - cond_type = p_config.get(CONF_CONDITION_TYPE, - DEFAULT_CONDITION_TYPE).lower() - action = _process_if(hass, config, p_config[CONF_CONDITION], - action, cond_type) + if CONF_CONDITION in p_config or CONF_CONDITION_TYPE in p_config: + action = _process_if(hass, config, p_config, action) if action is None: continue @@ -126,22 +125,32 @@ def _migrate_old_config(config): return new_conf -def _process_if(hass, config, if_configs, action, cond_type): +def _process_if(hass, config, p_config, action): """ Processes if checks. """ + cond_type = p_config.get(CONF_CONDITION_TYPE, + DEFAULT_CONDITION_TYPE).lower() + + if_configs = p_config.get(CONF_CONDITION) + use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES + + if use_trigger: + if_configs = p_config[CONF_TRIGGER] + if isinstance(if_configs, dict): if_configs = [if_configs] checks = [] for if_config in if_configs: - platform = _resolve_platform('condition', hass, config, + platform = _resolve_platform('if_action', hass, config, if_config.get(CONF_PLATFORM)) if platform is None: continue check = platform.if_action(hass, if_config) - if check is None: + # Invalid conditions are allowed if we base it on trigger + if check is None and not use_trigger: return None checks.append(check) @@ -177,15 +186,15 @@ def _process_trigger(hass, config, trigger_configs, name, action): _LOGGER.error("Error setting up rule %s", name) -def _resolve_platform(requester, hass, config, platform): +def _resolve_platform(method, hass, config, platform): """ Find automation platform. """ if platform is None: return None platform = prepare_setup_platform(hass, config, DOMAIN, platform) - if platform is None: + if platform is None or not hasattr(platform, method): _LOGGER.error("Unknown automation platform specified for %s: %s", - requester, platform) + method, platform) return None return platform diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index bb936d36a1b..8baa0a01d46 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -26,7 +26,7 @@ def trigger(hass, config, action): return False from_state = config.get(CONF_FROM, MATCH_ALL) - to_state = config.get(CONF_TO, MATCH_ALL) + to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL def state_automation_listener(entity, from_s, to_s): """ Listens for state changes and calls action. """ diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index a7afa183ba0..821295fdffa 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -22,6 +22,14 @@ WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] def trigger(hass, config, action): """ Listen for state changes based on `config`. """ + if CONF_AFTER in config: + after = dt_util.parse_time_str(config[CONF_AFTER]) + if after is None: + logging.getLogger(__name__).error( + 'Received invalid after value: %s', config[CONF_AFTER]) + return False + hours, minutes, seconds = after.hour, after.minute, after.second + hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) @@ -51,22 +59,22 @@ def if_action(hass, config): def time_if(): """ Validate time based if-condition """ now = dt_util.now() - if before is not None: - # Strip seconds if given - before_h, before_m = before.split(':')[0:2] + time = dt_util.parse_time_str(before) + if time is None: + return False - before_point = now.replace(hour=int(before_h), - minute=int(before_m)) + before_point = now.replace(hour=time.hour, minute=time.minute) if now > before_point: return False if after is not None: - # Strip seconds if given - after_h, after_m = after.split(':')[0:2] + time = dt_util.parse_time_str(after) + if time is None: + return False - after_point = now.replace(hour=int(after_h), minute=int(after_m)) + after_point = now.replace(hour=time.hour, minute=time.minute) if now < after_point: return False diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index d8fecf20db8..35795a7ae7f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -131,3 +131,20 @@ def date_str_to_date(dt_str): def strip_microseconds(dattim): """ Returns a copy of dattime object but with microsecond set to 0. """ return dattim.replace(microsecond=0) + + +def parse_time_str(time_str): + """ Parse a time string (00:20:00) into Time object. + Return None if invalid. + """ + parts = str(time_str).split(':') + if len(parts) < 2: + return None + try: + hour = int(parts[0]) + minute = int(parts[1]) + second = int(parts[2]) if len(parts) > 2 else 0 + return dt.time(hour, minute, second) + except ValueError: + # ValueError if value cannot be converted to an int or not in range + return None diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index df8b199b700..6a011a072a5 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -258,3 +258,67 @@ class TestAutomationEvent(unittest.TestCase): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(2, len(self.calls)) + + def test_using_trigger_as_condition(self): + """ """ + entity_id = 'test.entity' + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': [ + { + 'platform': 'state', + 'entity_id': entity_id, + 'state': 100 + }, + { + 'platform': 'numeric_state', + 'entity_id': entity_id, + 'below': 150 + } + ], + 'condition': 'use_trigger_values', + 'action': { + 'execute_service': 'test.automation', + } + } + }) + + self.hass.states.set(entity_id, 100) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, 120) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, 151) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_using_trigger_as_condition_with_invalid_condition(self): + """ Event is not a valid condition. Will it still work? """ + entity_id = 'test.entity' + self.hass.states.set(entity_id, 100) + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': [ + { + 'platform': 'event', + 'event_type': 'test_event', + }, + { + 'platform': 'numeric_state', + 'entity_id': entity_id, + 'below': 150 + } + ], + 'condition': 'use_trigger_values', + 'action': { + 'execute_service': 'test.automation', + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 991c3e066d4..b0410c75014 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -209,6 +209,24 @@ class TestAutomationState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_state_filter(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'state': 'world' + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_both_filters(self): self.assertTrue(automation.setup(self.hass, { automation.DOMAIN: { diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 96579af9aa8..f7187592c66 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -241,7 +241,6 @@ class TestAutomationTime(unittest.TestCase): fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) - self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -260,7 +259,6 @@ class TestAutomationTime(unittest.TestCase): fire_time_changed(self.hass, dt_util.utcnow().replace(minute=0)) - self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -279,7 +277,6 @@ class TestAutomationTime(unittest.TestCase): fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) - self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -301,7 +298,25 @@ class TestAutomationTime(unittest.TestCase): fire_time_changed(self.hass, dt_util.utcnow().replace( hour=0, minute=0, second=0)) - self.hass.states.set('test.entity', 'world') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_using_after(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'after': '5:00:00', + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace( + hour=5, minute=0, second=0)) + self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) From 08f2a67de449df779341019da6c39ce694a065f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 12:58:19 -0700 Subject: [PATCH 19/52] Allow falsy values for media player attributes --- homeassistant/components/media_player/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 88af4733c78..19ff0540c6b 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -484,7 +484,7 @@ class MediaPlayerDevice(Entity): else: state_attr = { attr: getattr(self, attr) for attr - in ATTR_TO_PROPERTY if getattr(self, attr) + in ATTR_TO_PROPERTY if getattr(self, attr) is not None } if self.media_image_url: From 77b9a1268788c7f36fd75a4143572d6f01870675 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Tue, 15 Sep 2015 21:07:49 -0400 Subject: [PATCH 20/52] Tags the name of the device to the end of the name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This helps the media player be more explicit about itself and what it is. It also namespaces it self a little better in the system. Rather than be `media_player.family_room` it is `media_player.family_room_apple_tv`. This helps for cases when there’s another actual media player like Kodi or Chromecast in there. --- homeassistant/components/media_player/itunes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index f63b6c933fe..891b7aff2b0 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -356,7 +356,10 @@ class AirPlayDevice(MediaPlayerDevice): self.player_state = state_hash.get('player_state', None) if 'name' in state_hash: - self.device_name = state_hash.get('name', 'AirPlay') + name = state_hash.get('name', '') + kind = state_hash.get('kind', 'AirPlay') + + self.device_name = (name + ' ' + kind).strip() if 'kind' in state_hash: self.kind = state_hash.get('kind', None) From 61685ea13d0c46916e7a92f1fa216568a6bd79ab Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Tue, 15 Sep 2015 21:40:39 -0400 Subject: [PATCH 21/52] tag on " AirTunes Speaker" instead --- homeassistant/components/media_player/itunes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 891b7aff2b0..ecbb144e033 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -357,9 +357,7 @@ class AirPlayDevice(MediaPlayerDevice): if 'name' in state_hash: name = state_hash.get('name', '') - kind = state_hash.get('kind', 'AirPlay') - - self.device_name = (name + ' ' + kind).strip() + self.device_name = (name + ' AirTunes Speaker').strip() if 'kind' in state_hash: self.kind = state_hash.get('kind', None) From 95eabe7c0ecd0bff40d9fe313b206a9b2d8e4a16 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 20:18:24 -0700 Subject: [PATCH 22/52] Freeze time for sun automation test --- tests/components/automation/test_sun.py | 97 ++++++++++++++----------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index dcb9cbafc1f..4781c5be79b 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -6,6 +6,7 @@ Tests sun automation. """ from datetime import datetime import unittest +from unittest.mock import patch import homeassistant.core as ha from homeassistant.components import sun @@ -38,19 +39,22 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_SETTING: '02:00:00 16-09-2015', }) + now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'sun', - 'event': 'sunset', - }, - 'action': { - 'execute_service': 'test.automation', + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + }, + 'action': { + 'execute_service': 'test.automation', + } } - } - })) + })) fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() @@ -61,19 +65,22 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', }) + now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'sun', - 'event': 'sunrise', - }, - 'action': { - 'execute_service': 'test.automation', + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunrise', + }, + 'action': { + 'execute_service': 'test.automation', + } } - } - })) + })) fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() @@ -84,20 +91,23 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_SETTING: '02:00:00 16-09-2015', }) + now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC) - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'sun', - 'event': 'sunset', - 'offset': '0:30:00' - }, - 'action': { - 'execute_service': 'test.automation', + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + 'offset': '0:30:00' + }, + 'action': { + 'execute_service': 'test.automation', + } } - } - })) + })) fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() @@ -108,20 +118,23 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', }) + now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) - self.assertTrue(automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'sun', - 'event': 'sunrise', - 'offset': '-0:30:00' - }, - 'action': { - 'execute_service': 'test.automation', + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunrise', + 'offset': '-0:30:00' + }, + 'action': { + 'execute_service': 'test.automation', + } } - } - })) + })) fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() From 5de89316b217b9d0c6aa6225a65294a82b6e39f4 Mon Sep 17 00:00:00 2001 From: Heath Paddock Date: Tue, 15 Sep 2015 22:58:46 -0500 Subject: [PATCH 23/52] Initial implementation of Foscam FI9821W support --- homeassistant/components/camera/foscam.py | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 homeassistant/components/camera/foscam.py diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py new file mode 100644 index 00000000000..e2cec19b170 --- /dev/null +++ b/homeassistant/components/camera/foscam.py @@ -0,0 +1,70 @@ +""" +Support for Foscam IP Cameras. + +This component provides basic support for Foscam IP cameras. + +As part of the basic support the following features will be provided: +-MJPEG video streaming +-Saving a snapshot +-Recording(JPEG frame capture) + +To use this component, add the following to your config/configuration.yaml: + +camera: + platform: foscam + name: Door Camera + username: visitor (a user with visitor/operator privilege is required, admin accounts oddly do not seem to work) + password: + ip: + +""" +import logging +from requests.auth import HTTPBasicAuth +from homeassistant.helpers import validate_config +from homeassistant.components.camera import DOMAIN +from homeassistant.components.camera import Camera +import requests +import re + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Adds a generic IP Camera. """ + if not validate_config({DOMAIN: config}, {DOMAIN: ['username', 'password', 'ip']}, + _LOGGER): + return None + + add_devices_callback([FoscamCamera(config)]) + + +# pylint: disable=too-many-instance-attributes +class FoscamCamera(Camera): + """ + A generic implementation of an IP camera that is reachable over a URL. + """ + + def __init__(self, device_info): + super().__init__() + self._name = device_info.get('name', 'Foscam Camera') + self._username = device_info.get('username') + self._password = device_info.get('password') + self._base_url = 'http://' + device_info.get('ip') + ':88/' + self._snap_picture_url = self._base_url + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' + self._username + '&pwd=' + self._password + + def camera_image(self): + """ Return a still image reponse from the camera """ + + response = requests.get(self._snap_picture_url) + pattern = re.compile('src="\.\.\/(.*\.jpg)"') + filename = pattern.search(response.content.decode("utf-8") ).group(1) + + response = requests.get(self._base_url + filename) + + return response.content + + @property + def name(self): + """ Return the name of this device """ + return self._name From 9678613a13e0ddd1cf2cfa4d2624adc8b858829b Mon Sep 17 00:00:00 2001 From: Heath Paddock Date: Tue, 15 Sep 2015 23:32:55 -0500 Subject: [PATCH 24/52] foscam: made 'port' configurable and added additional documentation --- homeassistant/components/camera/foscam.py | 54 ++++++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index e2cec19b170..28a4f7857f8 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -13,9 +13,42 @@ To use this component, add the following to your config/configuration.yaml: camera: platform: foscam name: Door Camera - username: visitor (a user with visitor/operator privilege is required, admin accounts oddly do not seem to work) - password: - ip: + ip: 192.168.0.123 + port: 88 + username: visitor + password: password + +camera 2: + name: 'Second Camera' + ... +camera 3: + name: 'Camera Three' + ... + + +VARIABLES: + +These are the variables for the device_data array: + +ip +*Required +The IP address of your foscam device + +username +*Required +THe username of a visitor or operator of your camera. Oddly admin accounts don't seem to have access to take snapshots + +password +*Required +the password for accessing your camera + +name +*Optional +This parameter allows you to override the name of your camera in homeassistant + +port +*Optional +The port that the camera is running on. The default is 88. """ import logging @@ -32,8 +65,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Adds a generic IP Camera. """ - if not validate_config({DOMAIN: config}, {DOMAIN: ['username', 'password', 'ip']}, - _LOGGER): + if not validate_config({DOMAIN: config}, {DOMAIN: ['username', 'password', 'ip']}, _LOGGER): return None add_devices_callback([FoscamCamera(config)]) @@ -42,7 +74,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-instance-attributes class FoscamCamera(Camera): """ - A generic implementation of an IP camera that is reachable over a URL. + An implementation of a Foscam IP camera. """ def __init__(self, device_info): @@ -50,16 +82,24 @@ class FoscamCamera(Camera): self._name = device_info.get('name', 'Foscam Camera') self._username = device_info.get('username') self._password = device_info.get('password') - self._base_url = 'http://' + device_info.get('ip') + ':88/' + + port = device_info.get('port', 88) + + self._base_url = 'http://' + device_info.get('ip') + ':' + str(port) + '/' self._snap_picture_url = self._base_url + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' + self._username + '&pwd=' + self._password + _LOGGER.info('Using the following URL for Foscam camera: ' + self._snap_picture_url) def camera_image(self): """ Return a still image reponse from the camera """ + # send the request to snap a picture response = requests.get(self._snap_picture_url) + + # parse the response to find the image file name pattern = re.compile('src="\.\.\/(.*\.jpg)"') filename = pattern.search(response.content.decode("utf-8") ).group(1) + # send request for the image response = requests.get(self._base_url + filename) return response.content From 90e21791f6b7538ceedf5a3ee9dfa8449ab21ac6 Mon Sep 17 00:00:00 2001 From: Heath Paddock Date: Tue, 15 Sep 2015 23:39:03 -0500 Subject: [PATCH 25/52] Removed obsolete code --- homeassistant/components/camera/foscam.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 28a4f7857f8..e9e4cd4a02b 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -5,8 +5,6 @@ This component provides basic support for Foscam IP cameras. As part of the basic support the following features will be provided: -MJPEG video streaming --Saving a snapshot --Recording(JPEG frame capture) To use this component, add the following to your config/configuration.yaml: @@ -52,7 +50,6 @@ The port that the camera is running on. The default is 88. """ import logging -from requests.auth import HTTPBasicAuth from homeassistant.helpers import validate_config from homeassistant.components.camera import DOMAIN from homeassistant.components.camera import Camera From 2fd7b98cabe3357e0ce32b1039c2edde630aeeb3 Mon Sep 17 00:00:00 2001 From: Heath Paddock Date: Tue, 15 Sep 2015 23:45:12 -0500 Subject: [PATCH 26/52] minor code cleanup --- homeassistant/components/camera/foscam.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index e9e4cd4a02b..d37740c73f5 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -61,7 +61,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """ Adds a generic IP Camera. """ + """ Adds a Foscam IP Camera. """ if not validate_config({DOMAIN: config}, {DOMAIN: ['username', 'password', 'ip']}, _LOGGER): return None @@ -76,15 +76,17 @@ class FoscamCamera(Camera): def __init__(self, device_info): super().__init__() - self._name = device_info.get('name', 'Foscam Camera') + + ip = device_info.get('ip') + port = device_info.get('port', 88) + + self._base_url = 'http://' + ip + ':' + str(port) + '/' self._username = device_info.get('username') self._password = device_info.get('password') - - port = device_info.get('port', 88) - - self._base_url = 'http://' + device_info.get('ip') + ':' + str(port) + '/' self._snap_picture_url = self._base_url + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' + self._username + '&pwd=' + self._password - _LOGGER.info('Using the following URL for Foscam camera: ' + self._snap_picture_url) + self._name = device_info.get('name', 'Foscam Camera') + + _LOGGER.info('Using the following URL for %s: %s', self._name, self._snap_picture_url) def camera_image(self): """ Return a still image reponse from the camera """ From 3dcd18af9e60f3a26278c4fd601c0bdc7dd63319 Mon Sep 17 00:00:00 2001 From: Heath Paddock Date: Wed, 16 Sep 2015 00:09:16 -0500 Subject: [PATCH 27/52] Fixed flake8 errors --- homeassistant/components/camera/foscam.py | 27 ++++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index d37740c73f5..16d25f7afeb 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -1,7 +1,7 @@ """ Support for Foscam IP Cameras. -This component provides basic support for Foscam IP cameras. +This component provides basic support for Foscam IP cameras. As part of the basic support the following features will be provided: -MJPEG video streaming @@ -34,15 +34,16 @@ The IP address of your foscam device username *Required -THe username of a visitor or operator of your camera. Oddly admin accounts don't seem to have access to take snapshots +The username of a visitor or operator of your camera. +Oddly admin accounts don't seem to have access to take snapshots. password *Required -the password for accessing your camera +The password for accessing your camera. name *Optional -This parameter allows you to override the name of your camera in homeassistant +This parameter allows you to override the name of your camera in homeassistant. port *Optional @@ -62,7 +63,8 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Adds a Foscam IP Camera. """ - if not validate_config({DOMAIN: config}, {DOMAIN: ['username', 'password', 'ip']}, _LOGGER): + if not validate_config({DOMAIN: config}, + {DOMAIN: ['username', 'password', 'ip']}, _LOGGER): return None add_devices_callback([FoscamCamera(config)]) @@ -76,17 +78,20 @@ class FoscamCamera(Camera): def __init__(self, device_info): super().__init__() - + ip = device_info.get('ip') port = device_info.get('port', 88) - + self._base_url = 'http://' + ip + ':' + str(port) + '/' self._username = device_info.get('username') self._password = device_info.get('password') - self._snap_picture_url = self._base_url + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' + self._username + '&pwd=' + self._password + self._snap_picture_url = self._base_url + + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' + + self._username + '&pwd=' + self._password self._name = device_info.get('name', 'Foscam Camera') - - _LOGGER.info('Using the following URL for %s: %s', self._name, self._snap_picture_url) + + _LOGGER.info('Using the following URL for %s: %s', + self._name, self._snap_picture_url) def camera_image(self): """ Return a still image reponse from the camera """ @@ -96,7 +101,7 @@ class FoscamCamera(Camera): # parse the response to find the image file name pattern = re.compile('src="\.\.\/(.*\.jpg)"') - filename = pattern.search(response.content.decode("utf-8") ).group(1) + filename = pattern.search(response.content.decode("utf-8")).group(1) # send request for the image response = requests.get(self._base_url + filename) From 5af16432976f72de1d86f1d725205c4ec6a6caa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 22:23:07 -0700 Subject: [PATCH 28/52] Add warning when entity not found in reproduce_state --- homeassistant/helpers/state.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index d87ee48930c..d4a18806a17 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -51,6 +51,8 @@ def reproduce_state(hass, states, blocking=False): current_state = hass.states.get(state.entity_id) if current_state is None: + _LOGGER.warning('reproduce_state: Unable to find entity %s', + state.entity_id) continue if state.state == STATE_ON: @@ -58,7 +60,8 @@ def reproduce_state(hass, states, blocking=False): elif state.state == STATE_OFF: service = SERVICE_TURN_OFF else: - _LOGGER.warning("Unable to reproduce state for %s", state) + _LOGGER.warning("reproduce_state: Unable to reproduce state %s", + state) continue service_data = dict(state.attributes) From 98feb3cd93300fe60346399e217def2983690f0a Mon Sep 17 00:00:00 2001 From: Heath Paddock Date: Wed, 16 Sep 2015 00:40:51 -0500 Subject: [PATCH 29/52] Fixed pylint errors --- homeassistant/components/camera/foscam.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 16d25f7afeb..21f4589ca6c 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -77,17 +77,17 @@ class FoscamCamera(Camera): """ def __init__(self, device_info): - super().__init__() + super(FoscamCamera, self).__init__() - ip = device_info.get('ip') + ip_address = device_info.get('ip') port = device_info.get('port', 88) - self._base_url = 'http://' + ip + ':' + str(port) + '/' + self._base_url = 'http://' + ip_address + ':' + str(port) + '/' self._username = device_info.get('username') self._password = device_info.get('password') - self._snap_picture_url = self._base_url - + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' - + self._username + '&pwd=' + self._password + self._snap_picture_url = self._base_url \ + + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' \ + + self._username + '&pwd=' + self._password self._name = device_info.get('name', 'Foscam Camera') _LOGGER.info('Using the following URL for %s: %s', @@ -100,7 +100,8 @@ class FoscamCamera(Camera): response = requests.get(self._snap_picture_url) # parse the response to find the image file name - pattern = re.compile('src="\.\.\/(.*\.jpg)"') + + pattern = re.compile('src="[.][.]/(.*[.]jpg)"') filename = pattern.search(response.content.decode("utf-8")).group(1) # send request for the image From 86aea83f644811053c7900c2833279c241cb2730 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Sep 2015 23:35:28 -0700 Subject: [PATCH 30/52] Device tracker improvements --- .../components/device_tracker/__init__.py | 17 ++++++------- homeassistant/util/__init__.py | 4 ++-- tests/components/device_tracker/test_init.py | 24 ++++++++++++------- tests/util/test_init.py | 6 ++--- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c7dc2593ddb..d33d182dd2c 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -176,10 +176,8 @@ class DeviceTracker(object): self.track_new = track_new self.lock = threading.Lock() - entity_ids = [] for device in devices: if device.track: - entity_ids.append(device.entity_id) device.update_ha_state() self.group = None @@ -194,9 +192,9 @@ class DeviceTracker(object): mac = mac.upper() device = self.mac_to_dev.get(mac) if not device: - dev_id = util.slugify(host_name or mac) + dev_id = util.slugify(host_name or '') or util.slugify(mac) else: - dev_id = str(dev_id) + dev_id = str(dev_id).lower() device = self.devices.get(dev_id) if device: @@ -234,7 +232,8 @@ class DeviceTracker(object): """ Update stale devices. """ with self.lock: for device in self.devices.values(): - if device.last_update_home and device.stale(now): + if (device.track and device.last_update_home and + device.stale(now)): device.update_ha_state(True) @@ -336,7 +335,8 @@ def convert_csv_config(csv_path, yaml_path): with open(csv_path) as inp: for row in csv.DictReader(inp): dev_id = util.ensure_unique_string( - util.slugify(row['name']) or DEVICE_DEFAULT_NAME, used_ids) + (util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(), + used_ids) used_ids.add(dev_id) device = Device(None, None, row['track'] == '1', dev_id, row['device'], row['name'], row['picture']) @@ -350,8 +350,9 @@ def load_config(path, hass, consider_home): return [] return [ Device(hass, consider_home, device.get('track', False), - str(dev_id), device.get('mac'), device.get('name'), - device.get('picture'), device.get(CONF_AWAY_HIDE, False)) + str(dev_id).lower(), str(device.get('mac')).upper(), + device.get('name'), device.get('picture'), + device.get(CONF_AWAY_HIDE, False)) for dev_id, device in load_yaml_config_file(path).items()] diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 5c77fa37814..805937376a0 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -21,7 +21,7 @@ from .dt import datetime_to_local_str, utcnow RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') -RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+') +RE_SLUGIFY = re.compile(r'[^a-z0-9_]+') def sanitize_filename(filename): @@ -36,7 +36,7 @@ def sanitize_path(path): def slugify(text): """ Slugifies a given text. """ - text = text.replace(" ", "_") + text = text.lower().replace(" ", "_") return RE_SLUGIFY.sub("", text) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 40f8a12c2d0..8b086e97c88 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -7,7 +7,7 @@ Tests the device tracker compoments. # pylint: disable=protected-access,too-many-public-methods import unittest from unittest.mock import patch -from datetime import timedelta +from datetime import datetime, timedelta import os from homeassistant.config import load_yaml_config_file @@ -127,7 +127,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) config = device_tracker.load_config(self.yaml_devices, self.hass, timedelta(seconds=0))[0] - self.assertEqual('DEV1', config.dev_id) + self.assertEqual('dev1', config.dev_id) self.assertEqual(True, config.track) def test_discovery(self): @@ -145,17 +145,25 @@ class TestComponentsDeviceTracker(unittest.TestCase): scanner.reset() scanner.come_home('DEV1') - self.assertTrue(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + register_time = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) + scan_time = datetime(2015, 9, 15, 23, 1, tzinfo=dt_util.UTC) + + with patch('homeassistant.components.device_tracker.dt_util.utcnow', + return_value=register_time): + self.assertTrue(device_tracker.setup(self.hass, { + 'device_tracker': { + 'platform': 'test', + 'consider_home': 59, + }})) + self.assertEqual(STATE_HOME, self.hass.states.get('device_tracker.dev1').state) scanner.leave_home('DEV1') - now = dt_util.utcnow().replace(second=0) + timedelta(hours=1) - - with patch('homeassistant.util.dt.utcnow', return_value=now): - fire_time_changed(self.hass, now) + with patch('homeassistant.components.device_tracker.dt_util.utcnow', + return_value=scan_time): + fire_time_changed(self.hass, scan_time) self.hass.pool.block_till_done() self.assertEqual(STATE_NOT_HOME, diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 8b5f115d03b..94358f5eb51 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -32,9 +32,9 @@ class TestUtil(unittest.TestCase): def test_slugify(self): """ Test slugify. """ - self.assertEqual("Test", util.slugify("T-!@#$!#@$!$est")) - self.assertEqual("Test_More", util.slugify("Test More")) - self.assertEqual("Test_More", util.slugify("Test_(More)")) + self.assertEqual("test", util.slugify("T-!@#$!#@$!$est")) + self.assertEqual("test_more", util.slugify("Test More")) + self.assertEqual("test_more", util.slugify("Test_(More)")) def test_split_entity_id(self): """ Test split_entity_id. """ From 3c3eadbef56d0ee986ac01d560626d07c96a4193 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2015 08:59:42 -0700 Subject: [PATCH 31/52] Update frontend with alarm ui --- homeassistant/components/frontend/version.py | 2 +- .../components/frontend/www_static/frontend.html | 16 ++++++++-------- .../frontend/www_static/home-assistant-polymer | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 41e727adf89..7b434017191 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "397aa7c09f4938b1358672c9983f9f32" +VERSION = "0ab148ece11ddde26b95460c2c91da3d" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 60831ab1e66..0434c21893b 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -4946,7 +4946,7 @@ paper-ripple { } .link { color: #03A9F4; - } \ No newline at end of file +console.group&&(console.groupCollapsed("Dispatch: %s",t),console.group("payload"),console.debug(e),console.groupEnd())},e.dispatchError=function(t){console.group&&(console.debug("Dispatch error: "+t),console.groupEnd())},e.storeHandled=function(t,e,n){console.group&&e!==n&&console.debug("Store "+t+" handled action")},e.dispatchEnd=function(t){console.group&&(console.debug("Dispatch done, new state: ",t.toJS()),console.groupEnd())}},function(t,e,n){function r(t,e){this.__prevState=t,this.__evaluator=e,this.__prevValues=i.Map(),this.__observers=[]}var i=n(2),o=n(7),u=n(8);Object.defineProperty(r.prototype,"notifyObservers",{writable:!0,configurable:!0,value:function(t){if(this.__observers.length>0){var e=i.Map();this.__observers.forEach(function(n){var r,i=n.getter,a=o(i),s=this.__prevState;this.__prevValues.has(a)?r=this.__prevValues.get(a):(r=this.__evaluator.evaluate(s,i),this.__prevValues=this.__prevValues.set(a,r));var c=this.__evaluator.evaluate(t,i);u(r,c)||(n.handler.call(null,c),e=e.set(a,c))}.bind(this)),this.__prevValues=e}this.__prevState=t}}),Object.defineProperty(r.prototype,"onChange",{writable:!0,configurable:!0,value:function(t,e){var n={getter:t,handler:e};return this.__observers.push(n),function(){var t=this.__observers.indexOf(n);t>-1&&this.__observers.splice(t,1)}.bind(this)}}),Object.defineProperty(r.prototype,"reset",{writable:!0,configurable:!0,value:function(t){this.__prevState=t,this.__prevValues=i.Map(),this.__observers=[]}}),t.exports=r},function(t,e,n){var r=n(2);t.exports=function(t,e){if(t.hasOwnProperty("__hashCode"))return t.__hashCode;var n=r.fromJS(t).hashCode();return e||(Object.defineProperty(t,"__hashCode",{enumerable:!1,configurable:!1,writable:!1,value:n}),Object.freeze(t)),n}},function(t,e,n){var r=n(2);t.exports=function(t,e){return r.is(t,e)}},function(t,e,n){function r(t){return s(t)&&a(t[t.length-1])}function i(t){return t[t.length-1]}function o(t){return t.slice(0,t.length-1)}function u(t){if(!c(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,l]}var a=n(3).isFunction,s=n(3).isArray,c=n(10).isKeyPath,l=function(t){return t};t.exports={isGetter:r,getComputeFn:i,getDeps:o,fromKeyPath:u}},function(t,e,n){var r=n(3).isArray,i=n(3).isFunction;e.isKeyPath=function(t){return r(t)&&!i(t[t.length-1])}},function(t,e,n){function r(){this.__cachedGetters=i.Map({})}var i=n(2),o=n(1).toImmutable,u=n(7),a=n(8),s=n(9).getComputeFn,c=n(9).getDeps,l=n(10).isKeyPath,f=n(9).isGetter,d=!1;Object.defineProperty(r.prototype,"evaluate",{writable:!0,configurable:!0,value:function(t,e){if(l(e))return t.getIn(e);if(!f(e))throw new Error("evaluate must be passed a keyPath or Getter");var n=u(e);if(this.__isCached(t,e))return this.__cachedGetters.getIn([n,"value"]);var r=c(e).map(function(e){return this.evaluate(t,e)}.bind(this));if(this.__hasStaleValue(t,e)){var i=this.__cachedGetters.getIn([n,"args"]);if(a(i,o(r))){var p=this.__cachedGetters.getIn([n,"value"]);return this.__cacheValue(t,e,i,p),p}}if(d===!0)throw d=!1,new Error("Evaluate may not be called within a Getters computeFn");var h;d=!0;try{h=s(e).apply(null,r),d=!1}catch(v){throw d=!1,v}return this.__cacheValue(t,e,r,h),h}}),Object.defineProperty(r.prototype,"__hasStaleValue",{writable:!0,configurable:!0,value:function(t,e){var n=u(e),r=this.__cachedGetters;return r.has(n)&&r.getIn([n,"stateHashCode"])!==t.hashCode()}}),Object.defineProperty(r.prototype,"__cacheValue",{writable:!0,configurable:!0,value:function(t,e,n,r){var a=u(e);this.__cachedGetters=this.__cachedGetters.set(a,i.Map({value:r,args:o(n),stateHashCode:t.hashCode()}))}}),Object.defineProperty(r.prototype,"__isCached",{writable:!0,configurable:!0,value:function(t,e){var n=u(e);return this.__cachedGetters.hasIn([n,"value"])&&this.__cachedGetters.getIn([n,"stateHashCode"])===t.hashCode()}}),Object.defineProperty(r.prototype,"untrack",{writable:!0,configurable:!0,value:function(t){}}),Object.defineProperty(r.prototype,"reset",{writable:!0,configurable:!0,value:function(){this.__cachedGetters=i.Map({})}}),t.exports=r},function(t,e,n){function r(t,e){var n={};return i(e,function(e,r){n[r]=t.evaluate(e)}),n}var i=n(3).each;t.exports=function(t){return{getInitialState:function(){return r(t,this.getDataBindings())},componentDidMount:function(){var e=this;e.__unwatchFns=[],i(this.getDataBindings(),function(n,r){var i=t.observe(n,function(t){var n={};n[r]=t,e.setState(n)});e.__unwatchFns.push(i)})},componentWillUnmount:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}},function(t,e,n){function r(t){return this instanceof r?(this.__handlers=o({}),t&&u(this,t),void this.initialize()):new r(t)}function i(t){return t instanceof r}var o=n(2).Map,u=n(3).extend,a=n(1).toJS,s=n(1).toImmutable;Object.defineProperty(r.prototype,"initialize",{writable:!0,configurable:!0,value:function(){}}),Object.defineProperty(r.prototype,"getInitialState",{writable:!0,configurable:!0,value:function(){return o()}}),Object.defineProperty(r.prototype,"handle",{writable:!0,configurable:!0,value:function(t,e,n){var r=this.__handlers.get(e);return"function"==typeof r?r.call(this,t,n,e):t}}),Object.defineProperty(r.prototype,"handleReset",{writable:!0,configurable:!0,value:function(t){return this.getInitialState()}}),Object.defineProperty(r.prototype,"on",{writable:!0,configurable:!0,value:function(t,e){this.__handlers=this.__handlers.set(t,e)}}),Object.defineProperty(r.prototype,"serialize",{writable:!0,configurable:!0,value:function(t){return a(t)}}),Object.defineProperty(r.prototype,"deserialize",{writable:!0,configurable:!0,value:function(t){return s(t)}}),t.exports=r,t.exports.isStore=i}])})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(176),u=r(o);e["default"]=u["default"](i.reactor),t.exports=e["default"]},function(t,e){"use strict";var n=function(t){var e,n={};if(!(t instanceof Object)||Array.isArray(t))throw new Error("keyMirror(...): Argument must be an object.");for(e in t)t.hasOwnProperty(e)&&(n[e]=e);return n};t.exports=n},function(t,e){"use strict";function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(185),o=r(i);e.callApi=o["default"]},function(t,e,n){"use strict";function r(t){return i(t)?t:Object(t)}var i=n(6);t.exports=r},function(t,e,n){"use strict";var r=n(20),i=n(12),o=n(13),u="[object Array]",a=Object.prototype,s=a.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==u};t.exports=l},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(201),u=i(o),a=n(202),s=r(a),c=u["default"];e.actions=c;var l=s;e.getters=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){t.registerStores({restApiCache:l["default"]})}function o(t){return[["restApiCache",t.entity],function(t){return!!t}]}function u(t){return[["restApiCache",t.entity],function(t){return t||s.toImmutable({})}]}function a(t){return function(e){return["restApiCache",t.entity,e]}}Object.defineProperty(e,"__esModule",{value:!0}),e.register=i,e.createHasDataGetter=o,e.createEntityMapGetter=u,e.createByIdGetter=a;var s=n(3),c=n(225),l=r(c),f=n(224),d=r(f);e.createApiActions=d["default"]},function(t,e){"use strict";function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e){"use strict";function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"partial-base",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1}},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},toggleMenu:function(){this.fire("open-menu")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({ENTITY_HISTORY_DATE_SELECTED:null,ENTITY_HISTORY_FETCH_START:null,ENTITY_HISTORY_FETCH_ERROR:null,ENTITY_HISTORY_FETCH_SUCCESS:null,RECENT_ENTITY_HISTORY_FETCH_START:null,RECENT_ENTITY_HISTORY_FETCH_ERROR:null,RECENT_ENTITY_HISTORY_FETCH_SUCCESS:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return null!=t&&o(i(t))}var i=n(49),o=n(12);t.exports=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(137),n(58),e["default"]=new o["default"]({is:"state-info",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({LOGBOOK_DATE_SELECTED:null,LOGBOOK_ENTRIES_FETCH_START:null,LOGBOOK_ENTRIES_FETCH_ERROR:null,LOGBOOK_ENTRIES_FETCH_SUCCESS:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(226),u=i(o),a=n(79),s=r(a),c=u["default"];e.actions=c;var l=s;e.getters=l},function(t,e,n){"use strict";function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(126);t.exports=r},function(t,e){"use strict";function n(t,e){return t?e.map(function(e){return e in t.attributes?"has-"+e:""}).join(" "):""}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({authAttempt:a["default"],authCurrent:c["default"],rememberAuth:f["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(188),a=i(u),s=n(189),c=i(s),l=n(190),f=i(l),d=n(186),p=r(d),h=n(187),v=r(h),_=p;e.actions=_;var y=v;e.getters=y},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var u=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(s){i=!0,o=s}finally{try{!r&&a["return"]&&a["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),a=function(){function t(t,e){for(var n=0;n-1&&t%1==0&&e>t}var r=/^\d+$/,i=9007199254740991;t.exports=n},function(t,e,n){"use strict";function r(t,e,n){if(!u(n))return!1;var r=typeof e;if("number"==r?i(n)&&o(e,n.length):"string"==r&&e in n){var a=n[e];return t===t?t===a:a!==a}return!1}var i=n(16),o=n(26),u=n(6);t.exports=r},function(t,e,n){"use strict";function r(t){return o(t)&&i(t)&&a.call(t,"callee")&&!s.call(t,"callee")}var i=n(16),o=n(13),u=Object.prototype,a=u.hasOwnProperty,s=u.propertyIsEnumerable;t.exports=r},function(t,e,n){"use strict";var r=n(20),i=n(16),o=n(6),u=n(123),a=r(Object,"keys"),s=a?function(t){var e=null==t?void 0:t.constructor;return"function"==typeof e&&e.prototype===t||"function"!=typeof t&&i(t)?u(t):o(t)?a(t):[]}:u;t.exports=s},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(62),a=r(u);e["default"]=new o["default"]({is:"domain-icon",properties:{domain:{type:String,value:""},state:{type:String,value:""}},computeIcon:function(t,e){return a["default"](t,e)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"loading-box"}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(177),a=r(u);n(33),n(60),n(175),n(172),n(174),n(173),e["default"]=new o["default"]({is:"state-card-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"}},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=a["default"](t);if(e&&a["default"](e)===r)n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("state-card-"+r);i.stateObj=t,n.appendChild(i)}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17),e["default"]=new o["default"]({is:"state-card-display",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e,n){function r(){y&&clearTimeout(y),p&&clearTimeout(p),g=0,p=y=m=void 0}function s(e,n){n&&clearTimeout(n),p=y=m=void 0,e&&(g=o(),h=t.apply(_,d),y||p||(d=_=void 0))}function c(){var t=e-(o()-v);0>=t||t>e?s(m,p):y=setTimeout(c,t)}function l(){s(O,y)}function f(){if(d=arguments,v=o(),_=this,m=O&&(y||!w),b===!1)var n=w&&!y;else{p||w||(g=v);var r=b-(v-g),i=0>=r||r>b;i?(p&&(p=clearTimeout(p)),g=v,h=t.apply(_,d)):p||(p=setTimeout(l,r))}return i&&y?y=clearTimeout(y):y||e===b||(y=setTimeout(c,e)),n&&(i=!0,h=t.apply(_,d)),!i||y||p||(d=_=void 0),h}var d,p,h,v,_,y,m,g=0,b=!1,O=!0;if("function"!=typeof t)throw new TypeError(u);if(e=0>e?0:+e||0,n===!0){var w=!0;O=!1}else i(n)&&(w=!!n.leading,b="maxWait"in n&&a(+n.maxWait||0,e),O="trailing"in n?!!n.trailing:O);return f.cancel=r,f}var i=n(68),o=n(181),u="Expected a function",a=Math.max;t.exports=r},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({SERVER_CONFIG_LOADED:null,COMPONENT_LOADED:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({serverComponent:a["default"],serverConfig:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(193),a=i(u),s=n(194),c=i(s),l=n(191),f=r(l),d=n(192),p=r(d),h=f;e.actions=h;var v=p;e.getters=v},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(203),u=i(o),a=n(204),s=r(a),c=u["default"];e.actions=c;var l=s;e.getters=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({NAVIGATE:null,SHOW_SIDEBAR:null,LOG_OUT:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({notifications:a["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(221),a=i(u),s=n(219),c=r(s),l=n(220),f=r(l),d=c;e.actions=d;var p=f;e.getters=p},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({streamStatus:a["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(233),a=i(u),s=n(229),c=r(s),l=n(230),f=r(l),d=c;e.actions=d;var p=f;e.getters=p},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=r(i);e["default"]=o["default"]({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({isFetchingData:a["default"],isSyncScheduled:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(235),a=i(u),s=n(236),c=i(s),l=n(234),f=r(l),d=n(82),p=r(d),h=f;e.actions=h;var v=p;e.getters=v},function(t,e){"use strict";function n(t){return t.getFullYear()+"-"+(t.getMonth()+1)+"-"+t.getDate()}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e){"use strict";function n(t){var e=t.split(" "),n=r(e,2),i=n[0],o=n[1],u=i.split(":"),a=r(u,3),s=a[0],c=a[1],l=a[2],f=o.split("-"),d=r(f,3),p=d[0],h=d[1],v=d[2];return new Date(Date.UTC(v,parseInt(h,10)-1,p,s,c,l))}Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(s){i=!0,o=s}finally{try{!r&&a["return"]&&a["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}();e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e,n){if(null!=t){void 0!==n&&n in i(t)&&(e=[n]);for(var r=0,o=e.length;null!=t&&o>r;)t=t[e[r++]];return r&&r==o?t:void 0}}var i=n(8);t.exports=r},function(t,e,n){"use strict";function r(t,e,n,a,s,c){return t===e?!0:null==t||null==e||!o(t)&&!u(e)?t!==t&&e!==e:i(t,e,r,n,a,s,c)}var i=n(102),o=n(6),u=n(13);t.exports=r},function(t,e,n){"use strict";function r(t,e){var n=-1,r=o(t)?Array(t.length):[];return i(t,function(t,i,o){r[++n]=e(t,i,o)}),r}var i=n(97),o=n(16);t.exports=r},function(t,e){"use strict";function n(t){return function(e){return null==e?void 0:e[t]}}t.exports=n},function(t,e,n){"use strict";var r=n(48),i=r("length");t.exports=i},function(t,e,n){"use strict";function r(t,e){var n=typeof t;if("string"==n&&a.test(t)||"number"==n)return!0;if(i(t))return!1;var r=!u.test(t);return r||null!=e&&t in o(e)}var i=n(9),o=n(8),u=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,a=/^\w*$/;t.exports=r},function(t,e,n){"use strict";function r(t){return t===t&&!i(t)}var i=n(6);t.exports=r},function(t,e,n){"use strict";function r(t){if(o(t))return t;var e=[];return i(t).replace(u,function(t,n,r,i){e.push(r?i.replace(a,"$1"):n||t)}),e}var i=n(109),o=n(9),u=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,a=/\\(\\)?/g;t.exports=r},function(t,e){"use strict";function n(t){return t}t.exports=n},function(t,e,n){"use strict";function r(t){return u(t)?i(t):o(t)}var i=n(48),o=n(106),u=n(50);t.exports=r},function(t,e,n){(function(t){"use strict";!function(e,n){t.exports=n()}(void 0,function(){function e(){return Ln.apply(null,arguments)}function n(t){Ln=t}function r(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,r=[];for(n=0;n0)for(n in Rn)r=Rn[n],i=e[r],"undefined"!=typeof i&&(t[r]=i);return t}function h(t){p(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),zn===!1&&(zn=!0,e.updateOffset(this),zn=!1)}function v(t){return t instanceof h||null!=t&&null!=t._isAMomentObject}function _(t){return 0>t?Math.ceil(t):Math.floor(t)}function y(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=_(e)),n}function m(t,e,n){var r,i=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),u=0;for(r=0;i>r;r++)(n&&t[r]!==e[r]||!n&&y(t[r])!==y(e[r]))&&u++;return u+o}function g(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function O(t){for(var e,n,r,i,o=0;o0;){if(r=w(i.slice(0,e).join("-")))return r;if(n&&n.length>=e&&m(i,n,!0)>=e-1)break;e--}o++}return null}function w(e){var n=null;if(!Hn[e]&&"undefined"!=typeof t&&t&&t.exports)try{n=Nn._abbr,!function(){var t=new Error('Cannot find module "./locale"');throw t.code="MODULE_NOT_FOUND",t}(),S(n)}catch(r){}return Hn[e]}function S(t,e){var n;return t&&(n="undefined"==typeof e?T(t):M(t,e),n&&(Nn=n)),Nn._abbr}function M(t,e){return null!==e?(e.abbr=t,Hn[t]=Hn[t]||new g,Hn[t].set(e),S(t),Hn[t]):(delete Hn[t],null)}function T(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Nn;if(!r(t)){if(e=w(t))return e;t=[t]}return O(t)}function j(t,e){var n=t.toLowerCase();Yn[n]=Yn[n+"s"]=Yn[e]=t}function E(t){return"string"==typeof t?Yn[t]||Yn[t.toLowerCase()]:void 0}function I(t){var e,n,r={};for(n in t)u(t,n)&&(e=E(n),e&&(r[e]=t[n]));return r}function P(t,n){return function(r){return null!=r?(C(this,t,r),e.updateOffset(this,n),this):D(this,t)}}function D(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function C(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function A(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=E(t),"function"==typeof this[t])return this[t](e);return this}function x(t,e,n){var r=""+Math.abs(t),i=e-r.length,o=t>=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+r}function k(t,e,n,r){var i=r;"string"==typeof r&&(i=function(){return this[r]()}),t&&(Bn[t]=i),e&&(Bn[e[0]]=function(){return x(i.apply(this,arguments),e[1],e[2])}),n&&(Bn[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),t)})}function L(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function N(t){var e,n,r=t.match(Gn);for(e=0,n=r.length;n>e;e++)Bn[r[e]]?r[e]=Bn[r[e]]:r[e]=L(r[e]);return function(i){var o="";for(e=0;n>e;e++)o+=r[e]instanceof Function?r[e].call(i,t):r[e];return o}}function R(t,e){return t.isValid()?(e=z(e,t.localeData()),Fn[e]=Fn[e]||N(e),Fn[e](t)):t.localeData().invalidDate()}function z(t,e){function n(t){return e.longDateFormat(t)||t}var r=5;for(Un.lastIndex=0;r>=0&&Un.test(t);)t=t.replace(Un,n),Un.lastIndex=0,r-=1;return t}function H(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function Y(t,e,n){or[t]=H(e)?e:function(t){return t&&n?n:e}}function G(t,e){return u(or,t)?or[t](e._strict,e._locale):new RegExp(U(t))}function U(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,r,i){return e||n||r||i}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function F(t,e){var n,r=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(r=function(t,n){n[e]=y(t)}),n=0;nr;r++){if(i=s([2e3,r]),n&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[r]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[r]||(o="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[r]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[r].test(t))return r;if(n&&"MMM"===e&&this._shortMonthsParse[r].test(t))return r;if(!n&&this._monthsParse[r].test(t))return r}}function $(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),q(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function Z(t){return null!=t?($(this,t),e.updateOffset(this,!0),this):D(this,"Month")}function X(){return q(this.year(),this.month())}function Q(t){var e,n=t._a;return n&&-2===l(t).overflow&&(e=n[sr]<0||n[sr]>11?sr:n[cr]<1||n[cr]>q(n[ar],n[sr])?cr:n[lr]<0||n[lr]>24||24===n[lr]&&(0!==n[fr]||0!==n[dr]||0!==n[pr])?lr:n[fr]<0||n[fr]>59?fr:n[dr]<0||n[dr]>59?dr:n[pr]<0||n[pr]>999?pr:-1,l(t)._overflowDayOfYear&&(ar>e||e>cr)&&(e=cr),l(t).overflow=e),t}function tt(t){e.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function et(t,e){var n=!0;return a(function(){return n&&(tt(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function nt(t,e){_r[t]||(tt(e),_r[t]=!0)}function rt(t){var e,n,r=t._i,i=yr.exec(r);if(i){for(l(t).iso=!0,e=0,n=mr.length;n>e;e++)if(mr[e][1].exec(r)){t._f=mr[e][0];break}for(e=0,n=gr.length;n>e;e++)if(gr[e][1].exec(r)){t._f+=(i[6]||" ")+gr[e][0];break}r.match(nr)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(t){var n=br.exec(t._i);return null!==n?void(t._d=new Date(+n[1])):(rt(t),void(t._isValid===!1&&(delete t._isValid,e.createFromInputFallback(t))))}function ot(t,e,n,r,i,o,u){var a=new Date(t,e,n,r,i,o,u);return 1970>t&&a.setFullYear(t),a}function ut(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function at(t){return st(t)?366:365}function st(t){return t%4===0&&t%100!==0||t%400===0}function ct(){return st(this.year())}function lt(t,e,n){var r,i=n-e,o=n-t.day();return o>i&&(o-=7),i-7>o&&(o+=7),r=Dt(t).add(o,"d"),{week:Math.ceil(r.dayOfYear()/7),year:r.year()}}function ft(t){return lt(t,this._week.dow,this._week.doy).week}function dt(){return this._week.dow}function pt(){return this._week.doy}function ht(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function vt(t){var e=lt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function _t(t,e,n,r,i){var o,u=6+i-r,a=ut(t,0,1+u),s=a.getUTCDay();return i>s&&(s+=7),n=null!=n?1*n:i,o=1+u+7*(e-1)-s+n,{year:o>0?t:t-1,dayOfYear:o>0?o:at(t-1)+o}}function yt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function mt(t,e,n){return null!=t?t:null!=e?e:n}function gt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,r,i,o=[];if(!t._d){for(r=gt(t),t._w&&null==t._a[cr]&&null==t._a[sr]&&Ot(t),t._dayOfYear&&(i=mt(t._a[ar],r[ar]),t._dayOfYear>at(i)&&(l(t)._overflowDayOfYear=!0),n=ut(i,0,t._dayOfYear),t._a[sr]=n.getUTCMonth(),t._a[cr]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=r[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[lr]&&0===t._a[fr]&&0===t._a[dr]&&0===t._a[pr]&&(t._nextDay=!0,t._a[lr]=0),t._d=(t._useUTC?ut:ot).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm), +t._nextDay&&(t._a[lr]=24)}}function Ot(t){var e,n,r,i,o,u,a;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,u=4,n=mt(e.GG,t._a[ar],lt(Dt(),1,4).year),r=mt(e.W,1),i=mt(e.E,1)):(o=t._locale._week.dow,u=t._locale._week.doy,n=mt(e.gg,t._a[ar],lt(Dt(),o,u).year),r=mt(e.w,1),null!=e.d?(i=e.d,o>i&&++r):i=null!=e.e?e.e+o:o),a=_t(n,r,i,u,o),t._a[ar]=a.year,t._dayOfYear=a.dayOfYear}function wt(t){if(t._f===e.ISO_8601)return void rt(t);t._a=[],l(t).empty=!0;var n,r,i,o,u,a=""+t._i,s=a.length,c=0;for(i=z(t._f,t._locale).match(Gn)||[],n=0;n0&&l(t).unusedInput.push(u),a=a.slice(a.indexOf(r)+r.length),c+=r.length),Bn[o]?(r?l(t).empty=!1:l(t).unusedTokens.push(o),V(o,r,t)):t._strict&&!r&&l(t).unusedTokens.push(o);l(t).charsLeftOver=s-c,a.length>0&&l(t).unusedInput.push(a),l(t).bigHour===!0&&t._a[lr]<=12&&t._a[lr]>0&&(l(t).bigHour=void 0),t._a[lr]=St(t._locale,t._a[lr],t._meridiem),bt(t),Q(t)}function St(t,e,n){var r;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(r=t.isPM(n),r&&12>e&&(e+=12),r||12!==e||(e=0),e):e}function Mt(t){var e,n,r,i,o;if(0===t._f.length)return l(t).invalidFormat=!0,void(t._d=new Date(NaN));for(i=0;io)&&(r=o,n=e));a(t,n||e)}function Tt(t){if(!t._d){var e=I(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function jt(t){var e=new h(Q(Et(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Et(t){var e=t._i,n=t._f;return t._locale=t._locale||T(t._l),null===e||void 0===n&&""===e?d({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),v(e)?new h(Q(e)):(r(n)?Mt(t):n?wt(t):i(e)?t._d=e:It(t),t))}function It(t){var n=t._i;void 0===n?t._d=new Date:i(n)?t._d=new Date(+n):"string"==typeof n?it(t):r(n)?(t._a=o(n.slice(0),function(t){return parseInt(t,10)}),bt(t)):"object"==typeof n?Tt(t):"number"==typeof n?t._d=new Date(n):e.createFromInputFallback(t)}function Pt(t,e,n,r,i){var o={};return"boolean"==typeof n&&(r=n,n=void 0),o._isAMomentObject=!0,o._useUTC=o._isUTC=i,o._l=n,o._i=t,o._f=e,o._strict=r,jt(o)}function Dt(t,e,n,r){return Pt(t,e,n,r,!1)}function Ct(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Dt();for(n=e[0],i=1;it&&(t=-t,n="-"),n+x(~~(t/60),2)+e+x(~~t%60,2)})}function Rt(t){var e=(t||"").match(nr)||[],n=e[e.length-1]||[],r=(n+"").match(Tr)||["-",0,0],i=+(60*r[1])+y(r[2]);return"+"===r[0]?i:-i}function zt(t,n){var r,o;return n._isUTC?(r=n.clone(),o=(v(t)||i(t)?+t:+Dt(t))-+r,r._d.setTime(+r._d+o),e.updateOffset(r,!1),r):Dt(t).local()}function Ht(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Yt(t,n){var r,i=this._offset||0;return null!=t?("string"==typeof t&&(t=Rt(t)),Math.abs(t)<16&&(t=60*t),!this._isUTC&&n&&(r=Ht(this)),this._offset=t,this._isUTC=!0,null!=r&&this.add(r,"m"),i!==t&&(!n||this._changeInProgress?ne(this,Zt(t-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?i:Ht(this)}function Gt(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Ut(t){return this.utcOffset(0,t)}function Ft(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ht(this),"m")),this}function Bt(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Rt(this._i)),this}function Vt(t){return t=t?Dt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function qt(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Wt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(p(t,this),t=Et(t),t._a){var e=t._isUTC?s(t._a):Dt(t._a);this._isDSTShifted=this.isValid()&&m(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Kt(){return!this._isUTC}function Jt(){return this._isUTC}function $t(){return this._isUTC&&0===this._offset}function Zt(t,e){var n,r,i,o=t,a=null;return Lt(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(a=jr.exec(t))?(n="-"===a[1]?-1:1,o={y:0,d:y(a[cr])*n,h:y(a[lr])*n,m:y(a[fr])*n,s:y(a[dr])*n,ms:y(a[pr])*n}):(a=Er.exec(t))?(n="-"===a[1]?-1:1,o={y:Xt(a[2],n),M:Xt(a[3],n),d:Xt(a[4],n),h:Xt(a[5],n),m:Xt(a[6],n),s:Xt(a[7],n),w:Xt(a[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(i=te(Dt(o.from),Dt(o.to)),o={},o.ms=i.milliseconds,o.M=i.months),r=new kt(o),Lt(t)&&u(t,"_locale")&&(r._locale=t._locale),r}function Xt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Qt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function te(t,e){var n;return e=zt(e,t),t.isBefore(e)?n=Qt(t,e):(n=Qt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function ee(t,e){return function(n,r){var i,o;return null===r||isNaN(+r)||(nt(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),o=n,n=r,r=o),n="string"==typeof n?+n:n,i=Zt(n,r),ne(this,i,t),this}}function ne(t,n,r,i){var o=n._milliseconds,u=n._days,a=n._months;i=null==i?!0:i,o&&t._d.setTime(+t._d+o*r),u&&C(t,"Date",D(t,"Date")+u*r),a&&$(t,D(t,"Month")+a*r),i&&e.updateOffset(t,u||a)}function re(t,e){var n=t||Dt(),r=zt(n,this).startOf("day"),i=this.diff(r,"days",!0),o=-6>i?"sameElse":-1>i?"lastWeek":0>i?"lastDay":1>i?"sameDay":2>i?"nextDay":7>i?"nextWeek":"sameElse";return this.format(e&&e[o]||this.localeData().calendar(o,this,Dt(n)))}function ie(){return new h(this)}function oe(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=v(t)?t:Dt(t),+this>+t):(n=v(t)?+t:+Dt(t),n<+this.clone().startOf(e))}function ue(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=v(t)?t:Dt(t),+t>+this):(n=v(t)?+t:+Dt(t),+this.clone().endOf(e)e-o?(n=t.clone().add(i-1,"months"),r=(e-o)/(o-n)):(n=t.clone().add(i+1,"months"),r=(e-o)/(n-o)),-(i+r)}function fe(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function de(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=Dt([2e3,1]).day(e),r="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(r.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Ue(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=Re(t,this.localeData()),this.add(t-e,"d")):e}function Fe(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Be(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function Ve(t,e){k(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qe(t,e){return e._meridiemParse}function We(t){return"p"===(t+"").toLowerCase().charAt(0)}function Ke(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Je(t,e){e[pr]=y(1e3*("0."+t))}function $e(){return this._isUTC?"UTC":""}function Ze(){return this._isUTC?"Coordinated Universal Time":""}function Xe(t){return Dt(1e3*t)}function Qe(){return Dt.apply(null,arguments).parseZone()}function tn(t,e,n){var r=this._calendar[t];return"function"==typeof r?r.call(e,n):r}function en(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function nn(){return this._invalidDate}function rn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function un(t,e,n,r){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,r):i.replace(/%d/i,t)}function an(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function sn(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function cn(t,e,n,r){var i=T(),o=s().set(r,e);return i[n](o,t)}function ln(t,e,n,r,i){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return cn(t,e,n,i);var o,u=[];for(o=0;r>o;o++)u[o]=cn(t,o,n,i);return u}function fn(t,e){return ln(t,e,"months",12,"month")}function dn(t,e){return ln(t,e,"monthsShort",12,"month")}function pn(t,e){return ln(t,e,"weekdays",7,"day")}function hn(t,e){return ln(t,e,"weekdaysShort",7,"day")}function vn(t,e){return ln(t,e,"weekdaysMin",7,"day")}function _n(){var t=this._data;return this._milliseconds=$r(this._milliseconds),this._days=$r(this._days),this._months=$r(this._months),t.milliseconds=$r(t.milliseconds),t.seconds=$r(t.seconds),t.minutes=$r(t.minutes),t.hours=$r(t.hours),t.months=$r(t.months),t.years=$r(t.years),this}function yn(t,e,n,r){var i=Zt(e,n);return t._milliseconds+=r*i._milliseconds,t._days+=r*i._days,t._months+=r*i._months,t._bubble()}function mn(t,e){return yn(this,t,e,1)}function gn(t,e){return yn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function On(){var t,e,n,r,i,o=this._milliseconds,u=this._days,a=this._months,s=this._data;return o>=0&&u>=0&&a>=0||0>=o&&0>=u&&0>=a||(o+=864e5*bn(Sn(a)+u),u=0,a=0),s.milliseconds=o%1e3,t=_(o/1e3),s.seconds=t%60,e=_(t/60),s.minutes=e%60,n=_(e/60),s.hours=n%24,u+=_(n/24),i=_(wn(u)),a+=i,u-=bn(Sn(i)),r=_(a/12),a%=12,s.days=u,s.months=a,s.years=r,this}function wn(t){return 4800*t/146097}function Sn(t){return 146097*t/4800}function Mn(t){var e,n,r=this._milliseconds;if(t=E(t),"month"===t||"year"===t)return e=this._days+r/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(Sn(this._months)),t){case"week":return e/7+r/6048e5;case"day":return e+r/864e5;case"hour":return 24*e+r/36e5;case"minute":return 1440*e+r/6e4;case"second":return 86400*e+r/1e3;case"millisecond":return Math.floor(864e5*e)+r;default:throw new Error("Unknown unit "+t)}}function Tn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*y(this._months/12)}function jn(t){return function(){return this.as(t)}}function En(t){return t=E(t),this[t+"s"]()}function In(t){return function(){return this._data[t]}}function Pn(){return _(this.days()/7)}function Dn(t,e,n,r,i){return i.relativeTime(e||1,!!n,t,r)}function Cn(t,e,n){var r=Zt(t).abs(),i=di(r.as("s")),o=di(r.as("m")),u=di(r.as("h")),a=di(r.as("d")),s=di(r.as("M")),c=di(r.as("y")),l=i0,l[4]=n,Dn.apply(null,l)}function An(t,e){return void 0===pi[t]?!1:void 0===e?pi[t]:(pi[t]=e,!0)}function xn(t){var e=this.localeData(),n=Cn(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function kn(){var t,e,n,r=hi(this._milliseconds)/1e3,i=hi(this._days),o=hi(this._months);t=_(r/60),e=_(t/60),r%=60,t%=60,n=_(o/12),o%=12;var u=n,a=o,s=i,c=e,l=t,f=r,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(u?u+"Y":"")+(a?a+"M":"")+(s?s+"D":"")+(c||l||f?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(f?f+"S":""):"P0D"}var Ln,Nn,Rn=e.momentProperties=[],zn=!1,Hn={},Yn={},Gn=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Un=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Fn={},Bn={},Vn=/\d/,qn=/\d\d/,Wn=/\d{3}/,Kn=/\d{4}/,Jn=/[+-]?\d{6}/,$n=/\d\d?/,Zn=/\d{1,3}/,Xn=/\d{1,4}/,Qn=/[+-]?\d{1,6}/,tr=/\d+/,er=/[+-]?\d+/,nr=/Z|[+-]\d\d:?\d\d/gi,rr=/[+-]?\d+(\.\d{1,3})?/,ir=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,or={},ur={},ar=0,sr=1,cr=2,lr=3,fr=4,dr=5,pr=6;k("M",["MM",2],"Mo",function(){return this.month()+1}),k("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),k("MMMM",0,0,function(t){return this.localeData().months(this,t)}),j("month","M"),Y("M",$n),Y("MM",$n,qn),Y("MMM",ir),Y("MMMM",ir),F(["M","MM"],function(t,e){e[sr]=y(t)-1}),F(["MMM","MMMM"],function(t,e,n,r){var i=n._locale.monthsParse(t,r,n._strict);null!=i?e[sr]=i:l(n).invalidMonth=t});var hr="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),vr="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),_r={};e.suppressDeprecationWarnings=!1;var yr=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mr=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],gr=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],br=/^\/?Date\((\-?\d+)/i;e.createFromInputFallback=et("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),k(0,["YY",2],0,function(){return this.year()%100}),k(0,["YYYY",4],0,"year"),k(0,["YYYYY",5],0,"year"),k(0,["YYYYYY",6,!0],0,"year"),j("year","y"),Y("Y",er),Y("YY",$n,qn),Y("YYYY",Xn,Kn),Y("YYYYY",Qn,Jn),Y("YYYYYY",Qn,Jn),F(["YYYYY","YYYYYY"],ar),F("YYYY",function(t,n){n[ar]=2===t.length?e.parseTwoDigitYear(t):y(t)}),F("YY",function(t,n){n[ar]=e.parseTwoDigitYear(t)}),e.parseTwoDigitYear=function(t){return y(t)+(y(t)>68?1900:2e3)};var Or=P("FullYear",!1);k("w",["ww",2],"wo","week"),k("W",["WW",2],"Wo","isoWeek"),j("week","w"),j("isoWeek","W"),Y("w",$n),Y("ww",$n,qn),Y("W",$n),Y("WW",$n,qn),B(["w","ww","W","WW"],function(t,e,n,r){e[r.substr(0,1)]=y(t)});var wr={dow:0,doy:6};k("DDD",["DDDD",3],"DDDo","dayOfYear"),j("dayOfYear","DDD"),Y("DDD",Zn),Y("DDDD",Wn),F(["DDD","DDDD"],function(t,e,n){n._dayOfYear=y(t)}),e.ISO_8601=function(){};var Sr=et("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=Dt.apply(null,arguments);return this>t?this:t}),Mr=et("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=Dt.apply(null,arguments);return t>this?this:t});Nt("Z",":"),Nt("ZZ",""),Y("Z",nr),Y("ZZ",nr),F(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Rt(t)});var Tr=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var jr=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Er=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Zt.fn=kt.prototype;var Ir=ee(1,"add"),Pr=ee(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var Dr=et("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});k(0,["gg",2],0,function(){return this.weekYear()%100}),k(0,["GG",2],0,function(){return this.isoWeekYear()%100}),De("gggg","weekYear"),De("ggggg","weekYear"),De("GGGG","isoWeekYear"),De("GGGGG","isoWeekYear"),j("weekYear","gg"),j("isoWeekYear","GG"),Y("G",er),Y("g",er),Y("GG",$n,qn),Y("gg",$n,qn),Y("GGGG",Xn,Kn),Y("gggg",Xn,Kn),Y("GGGGG",Qn,Jn),Y("ggggg",Qn,Jn),B(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,r){e[r.substr(0,2)]=y(t)}),B(["gg","GG"],function(t,n,r,i){n[i]=e.parseTwoDigitYear(t)}),k("Q",0,0,"quarter"),j("quarter","Q"),Y("Q",Vn),F("Q",function(t,e){e[sr]=3*(y(t)-1)}),k("D",["DD",2],"Do","date"),j("date","D"),Y("D",$n),Y("DD",$n,qn),Y("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),F(["D","DD"],cr),F("Do",function(t,e){e[cr]=y(t.match($n)[0],10)});var Cr=P("Date",!0);k("d",0,"do","day"),k("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),k("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),k("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),k("e",0,0,"weekday"),k("E",0,0,"isoWeekday"),j("day","d"),j("weekday","e"),j("isoWeekday","E"),Y("d",$n),Y("e",$n),Y("E",$n),Y("dd",ir),Y("ddd",ir),Y("dddd",ir),B(["dd","ddd","dddd"],function(t,e,n){var r=n._locale.weekdaysParse(t);null!=r?e.d=r:l(n).invalidWeekday=t}),B(["d","e","E"],function(t,e,n,r){e[r]=y(t)});var Ar="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),xr="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),kr="Su_Mo_Tu_We_Th_Fr_Sa".split("_");k("H",["HH",2],0,"hour"),k("h",["hh",2],0,function(){return this.hours()%12||12}),Ve("a",!0),Ve("A",!1),j("hour","h"),Y("a",qe),Y("A",qe),Y("H",$n),Y("h",$n),Y("HH",$n,qn),Y("hh",$n,qn),F(["H","HH"],lr),F(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),F(["h","hh"],function(t,e,n){e[lr]=y(t),l(n).bigHour=!0});var Lr=/[ap]\.?m?\.?/i,Nr=P("Hours",!0);k("m",["mm",2],0,"minute"),j("minute","m"),Y("m",$n),Y("mm",$n,qn),F(["m","mm"],fr);var Rr=P("Minutes",!1);k("s",["ss",2],0,"second"),j("second","s"),Y("s",$n),Y("ss",$n,qn),F(["s","ss"],dr);var zr=P("Seconds",!1);k("S",0,0,function(){return~~(this.millisecond()/100)}),k(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),k(0,["SSS",3],0,"millisecond"),k(0,["SSSS",4],0,function(){return 10*this.millisecond()}),k(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),k(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),k(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),k(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),k(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),j("millisecond","ms"),Y("S",Zn,Vn),Y("SS",Zn,qn),Y("SSS",Zn,Wn);var Hr;for(Hr="SSSS";Hr.length<=9;Hr+="S")Y(Hr,tr);for(Hr="S";Hr.length<=9;Hr+="S")F(Hr,Je);var Yr=P("Milliseconds",!1);k("z",0,0,"zoneAbbr"),k("zz",0,0,"zoneName");var Gr=h.prototype;Gr.add=Ir,Gr.calendar=re,Gr.clone=ie,Gr.diff=ce,Gr.endOf=Oe,Gr.format=pe,Gr.from=he,Gr.fromNow=ve,Gr.to=_e,Gr.toNow=ye,Gr.get=A,Gr.invalidAt=Pe,Gr.isAfter=oe,Gr.isBefore=ue,Gr.isBetween=ae,Gr.isSame=se,Gr.isValid=Ee,Gr.lang=Dr,Gr.locale=me,Gr.localeData=ge,Gr.max=Mr,Gr.min=Sr,Gr.parsingFlags=Ie,Gr.set=A,Gr.startOf=be,Gr.subtract=Pr,Gr.toArray=Te,Gr.toObject=je,Gr.toDate=Me,Gr.toISOString=de,Gr.toJSON=de,Gr.toString=fe,Gr.unix=Se,Gr.valueOf=we,Gr.year=Or,Gr.isLeapYear=ct,Gr.weekYear=Ae,Gr.isoWeekYear=xe,Gr.quarter=Gr.quarters=Ne,Gr.month=Z,Gr.daysInMonth=X,Gr.week=Gr.weeks=ht,Gr.isoWeek=Gr.isoWeeks=vt,Gr.weeksInYear=Le,Gr.isoWeeksInYear=ke,Gr.date=Cr,Gr.day=Gr.days=Ue,Gr.weekday=Fe,Gr.isoWeekday=Be,Gr.dayOfYear=yt,Gr.hour=Gr.hours=Nr,Gr.minute=Gr.minutes=Rr,Gr.second=Gr.seconds=zr,Gr.millisecond=Gr.milliseconds=Yr,Gr.utcOffset=Yt,Gr.utc=Ut,Gr.local=Ft,Gr.parseZone=Bt,Gr.hasAlignedHourOffset=Vt,Gr.isDST=qt,Gr.isDSTShifted=Wt,Gr.isLocal=Kt,Gr.isUtcOffset=Jt,Gr.isUtc=$t,Gr.isUTC=$t,Gr.zoneAbbr=$e,Gr.zoneName=Ze,Gr.dates=et("dates accessor is deprecated. Use date instead.",Cr),Gr.months=et("months accessor is deprecated. Use month instead",Z),Gr.years=et("years accessor is deprecated. Use year instead",Or),Gr.zone=et("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Gt);var Ur=Gr,Fr={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Br={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Vr="Invalid date",qr="%d",Wr=/\d{1,2}/,Kr={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Jr=g.prototype;Jr._calendar=Fr,Jr.calendar=tn,Jr._longDateFormat=Br,Jr.longDateFormat=en,Jr._invalidDate=Vr,Jr.invalidDate=nn,Jr._ordinal=qr,Jr.ordinal=rn,Jr._ordinalParse=Wr,Jr.preparse=on,Jr.postformat=on,Jr._relativeTime=Kr,Jr.relativeTime=un,Jr.pastFuture=an,Jr.set=sn,Jr.months=W,Jr._months=hr,Jr.monthsShort=K,Jr._monthsShort=vr,Jr.monthsParse=J,Jr.week=ft,Jr._week=wr,Jr.firstDayOfYear=pt,Jr.firstDayOfWeek=dt,Jr.weekdays=ze,Jr._weekdays=Ar,Jr.weekdaysMin=Ye,Jr._weekdaysMin=kr,Jr.weekdaysShort=He,Jr._weekdaysShort=xr,Jr.weekdaysParse=Ge,Jr.isPM=We,Jr._meridiemParse=Lr,Jr.meridiem=Ke,S("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===y(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),e.lang=et("moment.lang is deprecated. Use moment.locale instead.",S),e.langData=et("moment.langData is deprecated. Use moment.localeData instead.",T);var $r=Math.abs,Zr=jn("ms"),Xr=jn("s"),Qr=jn("m"),ti=jn("h"),ei=jn("d"),ni=jn("w"),ri=jn("M"),ii=jn("y"),oi=In("milliseconds"),ui=In("seconds"),ai=In("minutes"),si=In("hours"),ci=In("days"),li=In("months"),fi=In("years"),di=Math.round,pi={s:45,m:45,h:22,d:26,M:11},hi=Math.abs,vi=kt.prototype;vi.abs=_n,vi.add=mn,vi.subtract=gn,vi.as=Mn,vi.asMilliseconds=Zr,vi.asSeconds=Xr,vi.asMinutes=Qr,vi.asHours=ti,vi.asDays=ei,vi.asWeeks=ni,vi.asMonths=ri,vi.asYears=ii,vi.valueOf=Tn,vi._bubble=On,vi.get=En,vi.milliseconds=oi,vi.seconds=ui,vi.minutes=ai,vi.hours=si,vi.days=ci,vi.weeks=Pn,vi.months=li,vi.years=fi,vi.humanize=xn,vi.toISOString=kn,vi.toString=kn,vi.toJSON=kn,vi.locale=me,vi.localeData=ge,vi.toIsoString=et("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",kn),vi.lang=Dr,k("X",0,0,"unix"),k("x",0,0,"valueOf"),Y("x",er),Y("X",rr),F("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),F("x",function(t,e,n){n._d=new Date(y(t))}),e.version="2.10.6",n(Dt),e.fn=Ur,e.min=At,e.max=xt,e.utc=s,e.unix=Xe,e.months=fn,e.isDate=i,e.locale=S,e.invalid=d,e.duration=Zt,e.isMoment=v,e.weekdays=pn,e.parseZone=Qe,e.localeData=T,e.isDuration=Lt,e.monthsShort=dn,e.weekdaysMin=vn,e.defineLocale=M,e.weekdaysShort=hn,e.normalizeUnits=E,e.relativeTimeThreshold=An;var _i=e;return _i})}).call(e,n(130)(t))},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-card",properties:{title:{type:String},header:{type:String}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-label-badge",properties:{value:{type:String},icon:{type:String},label:{type:String},description:{type:String},image:{type:String,observe:"imageChanged"}},computeClasses:function(t){return t&&t.length>5?"value big":"value"}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(55),o=r(i),u=n(2),a=n(1),s=r(a),c=6e4,l=u.util.parseDateTime;e["default"]=new s["default"]({is:"relative-ha-datetime",properties:{datetime:{type:String,observer:"datetimeChanged"},datetimeObj:{type:Object,observer:"datetimeObjChanged"},parsedDateTime:{type:Object},relativeTime:{type:String,value:"not set"}},created:function(){this.updateRelative=this.updateRelative.bind(this)},attached:function(){this._interval=setInterval(this.updateRelative,c)},detached:function(){clearInterval(this._interval)},datetimeChanged:function(t){this.parsedDateTime=t?l(t):null,this.updateRelative()},datetimeObjChanged:function(t){this.parsedDateTime=t,this.updateRelative()},updateRelative:function(){this.relativeTime=this.parsedDateTime?o["default"](this.parsedDateTime).fromNow():""}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(31),n(148),n(147),e["default"]=new o["default"]({is:"state-history-charts",properties:{stateHistory:{type:Object},isLoadingData:{type:Boolean,value:!1},apiLoaded:{type:Boolean,value:!1},isLoading:{type:Boolean,computed:"computeIsLoading(isLoadingData, apiLoaded)"},groupedStateHistory:{type:Object,computed:"computeGroupedStateHistory(isLoading, stateHistory)"},isSingleDevice:{type:Boolean,computed:"computeIsSingleDevice(stateHistory)"}},computeIsSingleDevice:function(t){return t&&1===t.size},computeGroupedStateHistory:function(t,e){if(t||!e)return{line:[],timeline:[]};var n={},r=[];e.forEach(function(t){if(t&&0!==t.size){var e=t.find(function(t){return"unit_of_measurement"in t.attributes}),i=e?e.attributes.unit_of_measurement:!1;i?i in n?n[i].push(t.toArray()):n[i]=[t.toArray()]:r.push(t.toArray())}}),r=r.length>0&&r;var i=Object.keys(n).map(function(t){return[t,n[t]]});return{line:i,timeline:r}},googleApiLoaded:function(){var t=this;google.load("visualization","1",{packages:["timeline","corechart"],callback:function(){return t.apiLoaded=!0}})},computeContentClasses:function(t){return t?"loading":""},computeIsLoading:function(t,e){return t||!e},computeIsEmpty:function(t){return t&&0===t.size},extractUnit:function(t){return t[0]},extractData:function(t){return t[1]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(17),e["default"]=new u["default"]({is:"state-card-toggle",properties:{stateObj:{type:Object,observer:"stateObjChanged"},toggleChecked:{type:Boolean,value:!1}},ready:function(){this.forceStateChange()},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.state?this.turn_on():e||"off"===this.stateObj.state||this.turn_off()},stateObjChanged:function(t){t&&this.updateToggle(t)},updateToggle:function(t){this.toggleChecked=t&&"off"!==t.state},forceStateChange:function(){this.updateToggle(this.stateObj)},turn_on:function(){var t=this;i.serviceActions.callTurnOn(this.stateObj.entityId).then(function(){return t.forceStateChange()})},turn_off:function(){var t=this;i.serviceActions.callTurnOff(this.stateObj.entityId).then(function(){return t.forceStateChange()})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return i.reactor.evaluate(i.serviceGetters.canToggleEntity(t))}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=r;var i=n(2);t.exports=e["default"]},function(t,e){"use strict";function n(t,e){switch(t){case"homeassistant":return"home";case"group":return"homeassistant-24:group";case"device_tracker":return"social:person";case"switch":return"image:flash-on";case"alarm_control_panel":return e&&"disarmed"===e?"icons:lock-open":"icons:lock";case"media_player":var n="hardware:cast";return e&&"off"!==e&&"idle"!==e&&(n+="-connected"),n;case"sun":return"image:wb-sunny";case"light":return"image:wb-incandescent";case"simple_alarm":return"social:notifications";case"notify":return"announcement";case"thermostat":return"homeassistant-100:thermostat";case"sensor":return"visibility";case"configurator":return"settings";case"conversation":return"av:hearing";case"script":return"description";case"scene":return"social:pages";case"updater":return"update_available"===e?"icons:cloud-download":"icons:cloud-done";default:return"bookmark"}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return u["default"](t).format("LT")}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(55),u=r(o);t.exports=e["default"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e["default"]=function(t,e){r.authActions.validate(t,{rememberAuth:e,useStreaming:r.localStoragePreferences.useStreaming})},t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(184);t.exports=r},function(t,e){"use strict";function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){"use strict";function r(t){return i(t)&&a.call(t)==o}var i=n(68),o="[object Function]",u=Object.prototype,a=u.toString; +t.exports=r},function(t,e){"use strict";function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=["isLoadingEntityHistory"];e.isLoadingEntityHistory=i;var o=["currentEntityHistoryDate"];e.currentDate=o;var u=["entityHistory"];e.entityHistoryMap=u;var a=[o,u,function(t,e){return e.get(t)||r.toImmutable({})}];e.entityHistoryForCurrentDate=a;var s=[o,u,function(t,e){return!!e.get(t)}];e.hasDataForCurrentDate=s;var c=["recentEntityHistory"];e.recentEntityHistoryMap=c;var l=["recentEntityHistory"];e.recentEntityHistoryUpdatedMap=l},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({currentEntityHistoryDate:a["default"],entityHistory:c["default"],isLoadingEntityHistory:f["default"],recentEntityHistory:p["default"],recentEntityHistoryUpdated:v["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(196),a=i(u),s=n(197),c=i(s),l=n(198),f=i(l),d=n(199),p=i(d),h=n(200),v=i(h),_=n(195),y=r(_),m=n(69),g=r(m),b=y;e.actions=b;var O=g;e.getters=O},function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(t,e){for(var n=0;n6e4}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var u=n(222),a=n(242),s=i(a),c=n(244),l=i(c),f=n(246),d=i(f),p=n(23),h=r(p),v=n(36),_=r(v),y=n(10),m=r(y),g=n(70),b=r(g),O=n(37),w=r(O),S=n(207),M=r(S),T=n(73),j=r(T),E=n(76),I=r(E),P=n(39),D=r(P),C=n(19),A=r(C),x=n(40),k=r(x),L=n(42),N=r(L),R=n(239),z=r(R),H=n(11),Y=r(H),G=function U(){o(this,U);var t=s["default"]();Object.defineProperties(this,{demo:{value:!1,enumerable:!0},localStoragePreferences:{value:u.localStoragePreferences,enumerable:!0},reactor:{value:t,enumerable:!0},util:{value:d["default"],enumerable:!0},startLocalStoragePreferencesSync:{value:u.localStoragePreferences.startSync.bind(u.localStoragePreferences,t)},startUrlSync:{value:I.urlSync.startSync.bind(null,t)},stopUrlSync:{value:I.urlSync.stopSync.bind(null,t)}}),l["default"](this,t,{auth:h,config:_,entity:m,entityHistory:b,event:w,logbook:M,moreInfo:j,navigation:I,notification:D,service:A,stream:k,sync:N,voice:z,restApi:Y})};e["default"]=G,t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e,n){var r=t?t.length:0;return n&&o(t,e,n)&&(e=!1),r?i(t,e):[]}var i=n(98),o=n(27);t.exports=r},function(t,e){"use strict";function n(t){var e=t?t.length:0;return e?t[e-1]:void 0}t.exports=n},function(t,e,n){"use strict";function r(t,e,n,r){var s=t?t.length:0;return s?(null!=e&&"boolean"!=typeof e&&(r=n,n=u(t,e,r)?void 0:e,e=!1),n=null==n?n:i(n,r,3),e?a(t,n):o(t,n)):[]}var i=n(25),o=n(110),u=n(27),a=n(124);t.exports=r},function(t,e,n){"use strict";function r(t,e,n){var r=a(t)?i:u;return e=o(e,n,3),r(t,e)}var i=n(93),o=n(25),u=n(47),a=n(9);t.exports=r},function(t,e,n){"use strict";function r(t,e){return i(t,o(e))}var i=n(89),o=n(54);t.exports=r},function(t,e,n){"use strict";function r(t,e,n){if(null==t)return[];n&&s(t,e,n)&&(e=void 0);var r=-1;e=i(e,n,3);var c=o(t,function(t,n,i){return{criteria:e(t,n,i),index:++r,value:t}});return u(c,a)}var i=n(25),o=n(47),u=n(108),a=n(114),s=n(27);t.exports=r},function(t,e,n){(function(e){"use strict";function r(t){var e=t?t.length:0;for(this.data={hash:a(null),set:new u};e--;)this.push(t[e])}var i=n(113),o=n(20),u=o(e,"Set"),a=o(Object,"create");r.prototype.push=i,t.exports=r}).call(e,function(){return this}())},function(t,e){"use strict";function n(t,e){for(var n=-1,r=t.length,i=Array(r);++ne&&!o||!i||n&&!u&&a||r&&a)return 1;if(e>t&&!n||!a||o&&!r&&i||u&&i)return-1}return 0}t.exports=n},function(t,e,n){"use strict";var r=n(100),i=n(115),o=i(r);t.exports=o},function(t,e,n){"use strict";function r(t,e,n,c){c||(c=[]);for(var l=-1,f=t.length;++le&&(e=-e>i?0:i+e),n=void 0===n||n>i?i:+n||0,0>n&&(n+=i),i=e>n?0:n-e>>>0,e>>>=0;for(var o=Array(i);++r=a,f=l?u():null,d=[];f?(r=o,c=!1):(l=!1,f=e?[]:d);t:for(;++nc))return!1;for(;++s0;++rd;d++)f._columns[d]=[];var p=0;return n&&u(),c.keySeq().sortBy(function(t){return i(t)}).forEach(function(t){if("a"===t)return void(f._demo=!0);var n=i(t);n>=0&&10>n?f._badges.push.apply(f._badges,r(c.get(t)).sortBy(o).toArray()):"group"===t?c.get(t).filter(function(t){return!t.attributes.auto}).sortBy(o).forEach(function(t){var n=s.util.expandGroup(t,e);n.forEach(function(t){return l[t.entityId]=!0}),a(t.entityDisplay,n.toArray())}):a(t,r(c.get(t)).sortBy(o).toArray())}),f},computeShouldRenderColumn:function(t,e){return 0===t||e.length},computeShowIntroduction:function(t,e,n){return 0===t&&(e||n._demo)},computeShowHideInstruction:function(t,e){ +return t.size>0&&!0&&!e._demo},computeStatesOfCard:function(t,e){return t[e]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(30),n(134),n(58),e["default"]=new u["default"]({is:"logbook-entry",entityClicked:function(t){t.preventDefault(),i.moreInfoActions.selectEntity(this.entryObj.entityId)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(30),e["default"]=new u["default"]({is:"services-list",behaviors:[s["default"]],properties:{serviceDomains:{type:Array,bindNuclear:[i.serviceGetters.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.domain}).toJS()}]}},computeServices:function(t){return this.services.get(t).toArray()},serviceClicked:function(t){t.preventDefault(),this.fire("service-selected",{domain:t.model.domain.domain,service:t.model.service})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(90),o=r(i),u=n(86),a=r(u),s=n(88),c=r(s),l=n(91),f=r(l),d=n(1),p=r(d);e["default"]=new p["default"]({is:"state-history-chart-line",properties:{data:{type:Object,observer:"dataChanged"},unit:{type:String},isSingleDevice:{type:Boolean,value:!1},isAttached:{type:Boolean,value:!1,observer:"dataChanged"}},created:function(){this.style.display="block"},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){if(this.isAttached){for(var t=p["default"].dom(this),e=this.unit,n=this.data;t.lastChild;)t.removeChild(t.lastChild);if(0!==n.length){var r=new google.visualization.LineChart(this),i=new google.visualization.DataTable;i.addColumn({type:"datetime",id:"Time"});var u={legend:{position:"top"},titlePosition:"none",vAxes:{0:{title:e}},hAxis:{format:"H:mm"},lineWidth:1,chartArea:{left:"60",width:"95%"},explorer:{actions:["dragToZoom","rightClickToReset","dragToPan"],keepInBounds:!0,axis:"horizontal",maxZoomIn:.1}};this.isSingleDevice&&(u.legend.position="none",u.vAxes[0].title=null,u.chartArea.left=40,u.chartArea.height="80%",u.chartArea.top=5,u.enableInteractivity=!1);var s=o["default"](a["default"](n),"lastChangedAsDate");s=f["default"](c["default"](s,function(t){return t.getTime()}));for(var l=[],d=new Array(n.length),h=0;hnew Date&&(a=new Date);var s=0;n.forEach(function(e){if(0!==e.length){var n=e[0].entityDisplay,r=void 0,i=null,o=null;e.forEach(function(e){null!==i&&e.state!==i?(r=e.lastChangedAsDate,t(n,i,o,r),i=e.state,o=r):null===i&&(i=e.state,o=e.lastChangedAsDate)}),t(n,i,o,a),s++}}),r.draw(i,{height:55+42*s,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);e["default"]=new u["default"]({is:"stream-status",behaviors:[s["default"]],properties:{isStreaming:{type:Boolean,bindNuclear:i.streamGetters.isStreamingEvents},hasError:{type:Boolean,bindNuclear:i.streamGetters.hasStreamingEventsError}},toggleChanged:function(){this.isStreaming?i.streamActions.stop():i.streamActions.start()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(32),n(59),n(163);var c=["camera","configurator"];e["default"]=new u["default"]({is:"more-info-dialog",behaviors:[s["default"]],properties:{stateObj:{type:Object,bindNuclear:i.moreInfoGetters.currentEntity,observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:[i.moreInfoGetters.currentEntityHistory,function(t){return t?[t]:!1}]},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(_delayedDialogOpen, _isLoadingHistoryData)"},_isLoadingHistoryData:{type:Boolean,bindNuclear:i.entityHistoryGetters.isLoadingEntityHistory},hasHistoryComponent:{type:Boolean,bindNuclear:i.configGetters.isComponentLoaded("history"),observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:i.moreInfoGetters.isCurrentEntityHistoryStale,observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},_delayedDialogOpen:{type:Boolean,value:!1},_boundOnBackdropTap:{type:Function,value:function(){return this._onBackdropTap.bind(this)}}},computeIsLoadingHistoryData:function(t,e){return!t||e},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&i.entityHistoryActions.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(t){var e=this;return t?(this.showHistoryComponent=this.hasHistoryComponent&&-1===c.indexOf(this.stateObj.domain),void this.async(function(){e.fetchHistoryData(),e.dialogOpen=!0},10)):void(this.dialogOpen=!1)},dialogOpenChanged:function(t){var e=this;t?(this.$.dialog.backdropElement.addEventListener("click",this._boundOnBackdropTap),this.async(function(){return e._delayedDialogOpen=!0},10)):!t&&this.stateObj&&(i.moreInfoActions.deselectEntity(),this._delayedDialogOpen=!1)},_onBackdropTap:function(){this.$.dialog.backdropElement.removeEventListener("click",this._boundOnBackdropTap),this.dialogOpen=!1}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(4),u=r(o);n(142),n(158),n(157),n(156),n(153),n(154),n(155),n(159),n(150),e["default"]=new Polymer({is:"home-assistant-main",behaviors:[u["default"]],properties:{narrow:{type:Boolean,value:!1},activePane:{type:String,bindNuclear:i.navigationGetters.activePane,observer:"activePaneChanged"},isSelectedStates:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("states")},isSelectedHistory:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("history")},isSelectedLogbook:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("logbook")},isSelectedDevEvent:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("devEvent")},isSelectedDevState:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("devState")},isSelectedDevService:{type:Boolean,bindNuclear:i.navigationGetters.isActivePane("devService")},showSidebar:{type:Boolean,bindNuclear:i.navigationGetters.showSidebar}},listeners:{"open-menu":"openMenu","close-menu":"closeMenu"},openMenu:function(){this.narrow?this.$.drawer.openDrawer():i.navigationActions.showSidebar(!0)},closeMenu:function(){this.$.drawer.closeDrawer(),this.showSidebar&&i.navigationActions.showSidebar(!1)},activePaneChanged:function(){this.narrow&&this.$.drawer.closeDrawer()},attached:function(){i.startUrlSync()},computeForceNarrow:function(t,e){return t||!e},detached:function(){i.stopUrlSync()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(2),a=n(4),s=r(a),c=n(64),l=r(c);e["default"]=new o["default"]({is:"login-form",behaviors:[s["default"]],properties:{isValidating:{type:Boolean,observer:"isValidatingChanged",bindNuclear:u.authGetters.isValidating},isInvalid:{type:Boolean,bindNuclear:u.authGetters.isInvalidAttempt},errorMessage:{type:String,bindNuclear:u.authGetters.attemptErrorMessage}},listeners:{keydown:"passwordKeyDown","loginButton.click":"validatePassword"},observers:["validatingChanged(isValidating, isInvalid)"],validatingChanged:function(t,e){t||e||(this.$.passwordInput.value="")},isValidatingChanged:function(t){var e=this;t||this.async(function(){return e.$.passwordInput.focus()},10)},passwordKeyDown:function(t){13===t.keyCode?(this.validatePassword(),t.preventDefault()):this.isInvalid&&(this.isInvalid=!1)},validatePassword:function(){this.$.hideKeyboardOnFocus.focus(),l["default"](this.$.passwordInput.value,this.$.rememberLogin.checked)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(14),n(146),e["default"]=new u["default"]({is:"partial-dev-call-service",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},domain:{type:String,value:""},service:{type:String,value:""},serviceData:{type:String,value:""}},serviceSelected:function(t){this.domain=t.detail.domain,this.service=t.detail.service},callService:function(){var t=void 0;try{t=this.serviceData?JSON.parse(this.serviceData):{}}catch(e){return void alert("Error parsing JSON: "+e)}i.serviceActions.callService(this.domain,this.service,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(14),n(138),e["default"]=new u["default"]({is:"partial-dev-fire-event",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(t){this.eventType=t.detail.eventType},fireEvent:function(){var t=void 0;try{t=this.eventData?JSON.parse(this.eventData):{}}catch(e){return void alert("Error parsing JSON: "+e)}i.eventActions.fireEvent(this.eventType,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o);n(14),n(135),e["default"]=new u["default"]({is:"partial-dev-set-state",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(t){var e=t?JSON.stringify(t,null," "):"";this.$.inputData.value=e,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(t){var e=i.reactor.evaluate(i.entityGetters.byId(t.detail.entityId));this.entityId=e.entityId,this.state=e.state,this.stateAttributes=JSON.stringify(e.attributes,null," ")},handleSetState:function(){var t=void 0;try{t=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(e){return void alert("Error parsing JSON: "+e)}i.entityActions.save({entityId:this.entityId,state:this.state,attributes:t})},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(14),n(59),e["default"]=new u["default"]({is:"partial-history",behaviors:[s["default"]],properties:{narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:i.entityHistoryGetters.hasDataForCurrentDate,observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:i.entityHistoryGetters.entityHistoryForCurrentDate},isLoadingData:{type:Boolean,bindNuclear:i.entityHistoryGetters.isLoadingEntityHistory},selectedDate:{type:String,value:null,bindNuclear:i.entityHistoryGetters.currentDate}},isDataLoadedChanged:function(t){t||this.async(function(){return i.entityHistoryActions.fetchSelectedDate()},1)},handleRefreshClick:function(){i.entityHistoryActions.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new Pikaday({field:this.$.datePicker.inputElement,onSelect:i.entityHistoryActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(t){return"flex content "+(t?"narrow":"wide")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(14),n(141),n(31),e["default"]=new u["default"]({is:"partial-logbook",behaviors:[s["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:i.logbookGetters.currentDate},isLoading:{type:Boolean,bindNuclear:i.logbookGetters.isLoadingEntries},isStale:{type:Boolean,bindNuclear:i.logbookGetters.isCurrentStale,observer:"isStaleChanged"},entries:{type:Array,bindNuclear:[i.logbookGetters.currentEntries,function(t){return t.reverse().toArray()}]},datePicker:{type:Object}},isStaleChanged:function(t){var e=this;t&&this.async(function(){return i.logbookActions.fetchDate(e.selectedDate)},1)},handleRefresh:function(){i.logbookActions.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new Pikaday({field:this.$.datePicker.inputElement,onSelect:i.logbookActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(14),n(143),n(144),e["default"]=new u["default"]({is:"partial-zone",behaviors:[s["default"]],properties:{narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:i.syncGetters.isFetching},isStreaming:{type:Boolean,bindNuclear:i.streamGetters.isStreamingEvents},canListen:{type:Boolean,bindNuclear:[i.voiceGetters.isVoiceSupported,i.configGetters.isComponentLoaded("conversation"),function(t,e){return t&&e}]},isListening:{type:Boolean,bindNuclear:i.voiceGetters.isListening},showListenInterface:{type:Boolean,bindNuclear:[i.voiceGetters.isListening,i.voiceGetters.isTransmitting,function(t,e){return t||e}]},introductionLoaded:{type:Boolean,bindNuclear:i.configGetters.isComponentLoaded("introduction")},locationName:{type:String,bindNuclear:i.configGetters.locationName},showMenu:{type:Boolean,value:!1,observer:"windowChange"},states:{type:Object,bindNuclear:i.entityGetters.visibleEntityMap},columns:{type:Number}},created:function(){var t=this;this.windowChange=this.windowChange.bind(this);for(var e=[],n=0;5>n;n++)e.push(278+278*n);this.mqls=e.map(function(e){var n=window.matchMedia("(min-width: "+e+"px)");return n.addListener(t.windowChange),n})},detached:function(){var t=this;this.mqls.forEach(function(e){return e.removeListener(t.windowChange)})},windowChange:function(){var t=this.mqls.reduce(function(t,e){return t+e.matches},0);this.columns=Math.max(1,t-this.showMenu)},handleRefresh:function(){i.syncActions.fetchAll()},handleListenClick:function(){this.isListening?i.voiceActions.stop():i.voiceActions.listen()},computeDomains:function(t){return t.keySeq().toArray()},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},computeStatesOfDomain:function(t,e){return t.get(e).toArray()},computeListenButtonIcon:function(t){return t?"av:mic-off":"av:mic"},computeRefreshButtonClass:function(t){return t?"ha-spin":void 0},computeShowIntroduction:function(t,e){return t||0===e.size},toggleMenu:function(){this.fire("open-menu")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);e["default"]=new u["default"]({is:"notification-manager",behaviors:[s["default"]],properties:{text:{type:String,bindNuclear:i.notificationGetters.lastNotificationMessage,observer:"showNotification"}},showNotification:function(t){t&&this.$.toast.show()}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(21);r(a),e["default"]=new u["default"]({is:"more-info-alarm_control_panel",handleDisarmTap:function(t){this.callService("alarm_disarm",{code:this.entered_code})},handleHomeTap:function(t){this.callService("alarm_arm_home",{code:this.entered_code})},handleAwayTap:function(t){this.callService("alarm_arm_away",{code:this.entered_code})},properties:{entered_code:{type:String,value:""}},enteredCodeChanged:function(t){this.entered_code=t.target.value},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,i.serviceActions.callService("alarm_control_panel",t,n)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-camera",properties:{stateObj:{type:Object},dialogOpen:{type:Boolean}},imageLoaded:function(){this.fire("iron-resize")},computeCameraImageUrl:function(t){return t?"/api/camera_proxy_stream/"+this.stateObj.entityId:"data:image/gif;base64,R0lGODlhAQABAAAAACw="}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(31),e["default"]=new u["default"]({is:"more-info-configurator",behaviors:[s["default"]],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:i.streamGetters.isStreamingEvents},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"}},computeIsConfigurable:function(t){return"configure"===t.state},computeSubmitCaption:function(t){return t.attributes.submit_caption||"Set configuration"},submitClicked:function(){var t=this;this.isConfiguring=!0;var e={configure_id:this.stateObj.attributes.configure_id};i.serviceActions.callService("configurator","configure",e).then(function(){t.isConfiguring=!1,t.isStreaming||i.syncActions.fetchAll()},function(){t.isConfiguring=!1})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=n(178),a=r(u);n(164),n(165),n(169),n(162),n(170),n(168),n(166),n(167),n(161),n(171),n(160),e["default"]=new o["default"]({is:"more-info-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"}},dialogOpenChanged:function(t){var e=o["default"].dom(this);e.lastChild&&(e.lastChild.dialogOpen=t)},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=a["default"](t);if(e&&a["default"](e)===r)n.lastChild.dialogOpen=this.dialogOpen,n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("more-info-"+r);i.stateObj=t,i.dialogOpen=this.dialogOpen,n.appendChild(i)}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),u=["entity_picture","friendly_name","unit_of_measurement"];e["default"]=new o["default"]({is:"more-info-default",properties:{stateObj:{type:Object}},computeDisplayAttributes:function(t){return t?Object.keys(t.attributes).filter(function(t){return-1===u.indexOf(t)}):[]},getAttributeValue:function(t,e){return t.attributes[e]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(4),s=r(a);n(32),e["default"]=new u["default"]({is:"more-info-group",behaviors:[s["default"]],properties:{stateObj:{type:Object},states:{type:Array,bindNuclear:[i.moreInfoGetters.currentEntity,i.entityGetters.entityMap,function(t,e){return t?t.attributes.entity_id.map(e.get.bind(e)):[]}]}},updateStates:function(){this.states=this.stateObj&&this.stateObj.attributes.entity_id?stateStore.gets(this.stateObj.attributes.entity_id).toArray():[]}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(21),s=r(a);n(139);var c=["brightness","xy_color"];e["default"]=new u["default"]({is:"more-info-light",properties:{stateObj:{type:Object,observer:"stateObjChanged"},brightnessSliderValue:{type:Number,value:0}},stateObjChanged:function(t){var e=this;t&&"on"===t.state&&(this.brightnessSliderValue=t.attributes.brightness),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return s["default"](t,c)},brightnessSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||(0===e?i.serviceActions.callTurnOff(this.stateObj.entityId):i.serviceActions.callService("light","turn_on",{entity_id:this.stateObj.entityId,brightness:e}))},colorPicked:function(t){var e=t.detail.rgb;i.serviceActions.callService("light","turn_on",{entity_id:this.stateObj.entityId,rgb_color:[e.r,e.g,e.b]})}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(21),s=r(a),c=["volume_level"];e["default"]=new u["default"]({is:"more-info-media_player",properties:{stateObj:{type:Object,observer:"stateObjChanged"},isOff:{type:Boolean,value:!1},isPlaying:{type:Boolean,value:!1},isMuted:{type:Boolean,value:!1},volumeSliderValue:{type:Number,value:0},supportsPause:{type:Boolean,value:!1},supportsVolumeSet:{type:Boolean,value:!1},supportsVolumeMute:{type:Boolean,value:!1},supportsPreviousTrack:{type:Boolean,value:!1},supportsNextTrack:{type:Boolean,value:!1},supportsTurnOn:{type:Boolean,value:!1},supportsTurnOff:{type:Boolean,value:!1}},stateObjChanged:function(t){var e=this;t&&(this.isOff="off"===t.state,this.isPlaying="playing"===t.state,this.volumeSliderValue=100*t.attributes.volume_level,this.isMuted=t.attributes.is_volume_muted,this.supportsPause=0!==(1&t.attributes.supported_media_commands),this.supportsVolumeSet=0!==(4&t.attributes.supported_media_commands),this.supportsVolumeMute=0!==(8&t.attributes.supported_media_commands),this.supportsPreviousTrack=0!==(16&t.attributes.supported_media_commands),this.supportsNextTrack=0!==(32&t.attributes.supported_media_commands),this.supportsTurnOn=0!==(128&t.attributes.supported_media_commands),this.supportsTurnOff=0!==(256&t.attributes.supported_media_commands)),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return s["default"](t,c)},computeIsOff:function(t){return"off"===t.state},computeMuteVolumeIcon:function(t){return t?"av:volume-off":"av:volume-up"},computePlaybackControlIcon:function(){return this.isPlaying?this.supportsPause?"av:pause":"av:stop":"av:play-arrow"},computeHidePowerButton:function(t,e,n){return t?!e:!n},handleTogglePower:function(){this.callService(this.isOff?"turn_on":"turn_off")},handlePrevious:function(){this.callService("media_previous_track")},handlePlaybackControl:function(){this.callService("media_play_pause")},handleNext:function(){this.callService("media_next_track")},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},volumeSliderChanged:function(t){var e=parseFloat(t.target.value),n=e>0?e/100:0;this.callService("volume_set",{volume_level:n})},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,i.serviceActions.callService("media_player",t,n)}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-script",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(63),u=r(o),a=i.util.parseDateTime;e["default"]=new Polymer({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object,computed:"computeSetting(stateObj)"}},computeRising:function(t){return a(t.attributes.next_rising)},computeSetting:function(t){return a(t.attributes.next_setting)},computeOrder:function(t,e){return t>e?["set","ris"]:["ris","set"]},itemCaption:function(t){return"ris"===t?"Rising ":"Setting "},itemDate:function(t){return"ris"===t?this.risingDate:this.settingDate},itemValue:function(t){return u["default"](this.itemDate(t))}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=n(1),u=r(o),a=n(21),s=r(a),c=["away_mode"];e["default"]=new u["default"]({is:"more-info-thermostat",properties:{stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(t){this.targetTemperatureSliderValue=t.state,this.awayToggleChecked="on"===t.attributes.away_mode,this.tempMin=t.attributes.min_temp,this.tempMax=t.attributes.max_temp},computeClassNames:function(t){return s["default"](t,c)},targetTemperatureSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||i.serviceActions.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:e})},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):e||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(t){var e=this;i.serviceActions.callService("thermostat","set_away_mode",{away_mode:t,entity_id:this.stateObj.entityId}).then(function(){return e.stateObjChanged(e.stateObj)})}}),t.exports=e["default"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e["default"]=new Polymer({is:"more-info-updater",properties:{stateObj:{type:Object}},updateTapped:function(){r.serviceActions.callService("updater","update",{})},linkTapped:function(){window.open(this.stateObj.attributes.link,"_blank")}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17),n(33),e["default"]=new o["default"]({is:"state-card-configurator",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17);var u=["playing","paused"];e["default"]=new o["default"]({is:"state-card-media_player",properties:{stateObj:{type:Object},isPlaying:{type:Boolean,computed:"computeIsPlaying(stateObj)"}},computeIsPlaying:function(t){return-1!==u.indexOf(t.state)},computePrimaryText:function(t,e){return e?t.attributes.media_title:t.stateDisplay},computeSecondaryText:function(t){var e=void 0;return"music"===t.attributes.media_content_type?t.attributes.media_artist:"tvshow"===t.attributes.media_content_type?(e=t.attributes.media_series_title,t.attributes.media_season&&t.attributes.media_episode&&(e+=" S"+t.attributes.media_season+"E"+t.attributes.media_episode),e):t.attributes.app_name?t.attributes.app_name:""}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(33),n(60),e["default"]=new o["default"]({is:"state-card-scene",properties:{stateObj:{type:Object},allowToggle:{type:Boolean,value:!1,computed:"computeAllowToggle(stateObj)"}},computeAllowToggle:function(t){return"off"===t.state||t.attributes.active_requested}}),t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17),e["default"]=new o["default"]({is:"state-card-thermostat",properties:{stateObj:{type:Object}}}),t.exports=e["default"]},function(t,e){"use strict";function n(t){return{attached:function(){var e=this;this.__unwatchFns=Object.keys(this.properties).reduce(function(n,r){if(!("bindNuclear"in e.properties[r]))return n;var i=e.properties[r].bindNuclear;if(!i)throw new Error("Undefined getter specified for key "+r);return e[r]=t.evaluate(i),n.concat(t.observe(i,function(t){e[r]=t}))},[])},detached:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return-1!==a.indexOf(t.domain)?t.domain:u["default"](t.entityId)?"toggle":"display"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(61),u=r(o),a=["thermostat","configurator","scene","media_player"];t.exports=e["default"]},function(t,e){"use strict";function n(t){return-1!==r.indexOf(t.domain)?t.domain:"default"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n;var r=["light","group","sun","configurator","thermostat","script","media_player","camera","updater","alarm_control_panel"];t.exports=e["default"]},function(t,e){"use strict";function n(t,e,n){var r=1-t-e,i=n/255,o=i/e*t,u=i/e*r,a=1.612*o-.203*i-.302*u,s=.509*-o+1.412*i+.066*u,c=.026*o-.072*i+.962*u;a=.0031308>=a?12.92*a:1.055*Math.pow(a,1/2.4)-.055,s=.0031308>=s?12.92*s:1.055*Math.pow(s,1/2.4)-.055,c=.0031308>=c?12.92*c:1.055*Math.pow(c,1/2.4)-.055;var l=Math.max(a,s,c);return a/=l,s/=l,c/=l,a=255*a,0>a&&(a=255),s=255*s,0>s&&(s=255),c=255*c,0>c&&(c=255),[a,s,c]}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){var r;(function(t,i,o){"use strict";(function(){function u(t){return"function"==typeof t||"object"==typeof t&&null!==t}function a(t){return"function"==typeof t}function s(t){return"object"==typeof t&&null!==t}function c(t){W=t}function l(t){Z=t}function f(){return function(){t.nextTick(_)}}function d(){return function(){q(_)}}function p(){var t=0,e=new tt(_),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){ +n.data=t=++t%2}}function h(){var t=new MessageChannel;return t.port1.onmessage=_,function(){t.port2.postMessage(0)}}function v(){return function(){setTimeout(_,1)}}function _(){for(var t=0;$>t;t+=2){var e=rt[t],n=rt[t+1];e(n),rt[t]=void 0,rt[t+1]=void 0}$=0}function y(){try{var t=n(251);return q=t.runOnLoop||t.runOnContext,d()}catch(e){return v()}}function m(){}function g(){return new TypeError("You cannot resolve a promise with itself")}function b(){return new TypeError("A promises callback cannot return that same promise.")}function O(t){try{return t.then}catch(e){return at.error=e,at}}function w(t,e,n,r){try{t.call(e,n,r)}catch(i){return i}}function S(t,e,n){Z(function(t){var r=!1,i=w(n,e,function(n){r||(r=!0,e!==n?j(t,n):I(t,n))},function(e){r||(r=!0,P(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&i&&(r=!0,P(t,i))},t)}function M(t,e){e._state===ot?I(t,e._result):e._state===ut?P(t,e._result):D(e,void 0,function(e){j(t,e)},function(e){P(t,e)})}function T(t,e){if(e.constructor===t.constructor)M(t,e);else{var n=O(e);n===at?P(t,at.error):void 0===n?I(t,e):a(n)?S(t,e,n):I(t,e)}}function j(t,e){t===e?P(t,g()):u(e)?T(t,e):I(t,e)}function E(t){t._onerror&&t._onerror(t._result),C(t)}function I(t,e){t._state===it&&(t._result=e,t._state=ot,0!==t._subscribers.length&&Z(C,t))}function P(t,e){t._state===it&&(t._state=ut,t._result=e,Z(E,t))}function D(t,e,n,r){var i=t._subscribers,o=i.length;t._onerror=null,i[o]=e,i[o+ot]=n,i[o+ut]=r,0===o&&t._state&&Z(C,t)}function C(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,i,o=t._result,u=0;uu;u++)D(r.resolve(t[u]),void 0,e,n);return i}function H(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(m);return j(n,t),n}function Y(t){var e=this,n=new e(m);return P(n,t),n}function G(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function U(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function F(t){this._id=ht++,this._state=void 0,this._result=void 0,this._subscribers=[],m!==t&&(a(t)||G(),this instanceof F||U(),L(this,t))}function B(){var t;if("undefined"!=typeof i)t=i;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=vt)}var V;V=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var q,W,K,J=V,$=0,Z=({}.toString,function(t,e){rt[$]=t,rt[$+1]=e,$+=2,2===$&&(W?W(_):K())}),X="undefined"!=typeof window?window:void 0,Q=X||{},tt=Q.MutationObserver||Q.WebKitMutationObserver,et="undefined"!=typeof t&&"[object process]"==={}.toString.call(t),nt="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,rt=new Array(1e3);K=et?f():tt?p():nt?h():void 0===X?y():v();var it=void 0,ot=1,ut=2,at=new A,st=new A;N.prototype._validateInput=function(t){return J(t)},N.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},N.prototype._init=function(){this._result=new Array(this.length)};var ct=N;N.prototype._enumerate=function(){for(var t=this,e=t.length,n=t.promise,r=t._input,i=0;n._state===it&&e>i;i++)t._eachEntry(r[i],i)},N.prototype._eachEntry=function(t,e){var n=this,r=n._instanceConstructor;s(t)?t.constructor===r&&t._state!==it?(t._onerror=null,n._settledAt(t._state,e,t._result)):n._willSettleAt(r.resolve(t),e):(n._remaining--,n._result[e]=t)},N.prototype._settledAt=function(t,e,n){var r=this,i=r.promise;i._state===it&&(r._remaining--,t===ut?P(i,n):r._result[e]=n),0===r._remaining&&I(i,r._result)},N.prototype._willSettleAt=function(t,e){var n=this;D(t,void 0,function(t){n._settledAt(ot,e,t)},function(t){n._settledAt(ut,e,t)})};var lt=R,ft=z,dt=H,pt=Y,ht=0,vt=F;F.all=lt,F.race=ft,F.resolve=dt,F.reject=pt,F._setScheduler=c,F._setAsap=l,F._asap=Z,F.prototype={constructor:F,then:function(t,e){var n=this,r=n._state;if(r===ot&&!t||r===ut&&!e)return this;var i=new this.constructor(m),o=n._result;if(r){var u=arguments[r-1];Z(function(){k(r,i,u,o)})}else D(n,i,t,e);return i},"catch":function(t){return this.then(null,t)}};var _t=B,yt={Promise:vt,polyfill:_t};n(250).amd?(r=function(){return yt}.call(e,n,e,o),!(void 0!==r&&(o.exports=r))):"undefined"!=typeof o&&o.exports?o.exports=yt:"undefined"!=typeof this&&(this.ES6Promise=yt),_t()}).call(void 0)}).call(e,n(247),function(){return this}(),n(248)(t))},function(t,e,n){"use strict";var r=n(65),i=r(Date,"now"),o=i||function(){return(new Date).getTime()};t.exports=o},function(t,e){"use strict";function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e,n){"use strict";var r=n(65),i=n(182),o=n(66),u="[object Array]",a=Object.prototype,s=a.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==u};t.exports=l},function(t,e,n){"use strict";function r(t){return null==t?!1:i(t)?l.test(s.call(t)):o(t)&&u.test(t)}var i=n(67),o=n(66),u=/^\[object .+?Constructor\]$/,a=Object.prototype,s=Function.prototype.toString,c=a.hasOwnProperty,l=RegExp("^"+s.call(c).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(180),i=n(23),o=function(t,e,n){var o=arguments.length<=3||void 0===arguments[3]?null:arguments[3],u=t.evaluate(i.getters.authInfo),a=u.host+"/api/"+n;return new r.Promise(function(t,n){var r=new XMLHttpRequest;r.open(e,a,!0),r.setRequestHeader("X-HA-access",u.authToken),r.onload=function(){if(r.status>199&&r.status<300)t(JSON.parse(r.responseText));else try{n(JSON.parse(r.responseText))}catch(e){n({})}},r.onerror=function(){return n({})},o?r.send(JSON.stringify(o)):r.send()})};e["default"]=o,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(s.getters.isSupported):r,o=n.rememberAuth,u=void 0===o?!1:o,f=n.host,d=void 0===f?"":f;t.dispatch(a["default"].VALIDATING_AUTH_TOKEN,{authToken:e,host:d}),c.actions.fetchAll(t).then(function(){t.dispatch(a["default"].VALID_AUTH_TOKEN,{authToken:e,host:d,rememberAuth:u}),i?s.actions.start(t,{syncOnInitialConnect:!1}):c.actions.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?l:n;t.dispatch(a["default"].INVALID_AUTH_TOKEN,{errorMessage:r})})}function o(t){t.dispatch(a["default"].LOG_OUT,{})}Object.defineProperty(e,"__esModule",{value:!0}),e.validate=i,e.logOut=o;var u=n(22),a=r(u),s=n(40),c=n(42),l="Unexpected result from API"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=[["authAttempt","isValidating"],function(t){return!!t}];e.isValidating=n;var r=[["authAttempt","isInvalid"],function(t){return!!t}];e.isInvalidAttempt=r;var i=["authAttempt","errorMessage"];e.attemptErrorMessage=i;var o=["rememberAuth"];e.rememberAuth=o;var u=[["authAttempt","authToken"],["authAttempt","host"],function(t,e){return{authToken:t,host:e}}];e.attemptAuthInfo=u;var a=["authCurrent","authToken"];e.currentAuthToken=a;var s=[a,["authCurrent","host"],function(t,e){return{authToken:t,host:e}}];e.currentAuthInfo=s;var c=[n,["authAttempt","authToken"],["authCurrent","authToken"],function(t,e,n){return t?e:n}];e.authToken=c;var l=[n,u,s,function(t,e,n){return t?e:n}];e.authInfo=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(null==t)throw new TypeError("Cannot destructure undefined")}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function u(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function a(t,e){var n=e.authToken,r=e.host;return d.toImmutable({authToken:n,host:r,isValidating:"true",isInvalid:!1,errorMessage:""})}function s(t,e){return i(e),_.getInitialState()}function c(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid","true").set("errorMessage",n)})}Object.defineProperty(e,"__esModule",{value:!0});var l=function(){function t(t,e){for(var n=0;n1&&t.set(p,r)})}function a(){return v.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var s=function(){function t(t,e){for(var n=0;no}Object.defineProperty(e,"__esModule",{value:!0});var i=n(3),o=6e4,u=["currentLogbookDate"];e.currentDate=u;var a=[u,["logbookEntriesUpdated"],function(t,e){return r(e.get(t))}];e.isCurrentStale=a;var s=[u,["logbookEntries"],function(t,e){return e.get(t)||i.toImmutable([])}];e.currentEntries=s;var c=["isLoadingLogbookEntries"];e.isLoadingEntries=c},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({currentLogbookDate:a["default"],isLoadingLogbookEntries:c["default"],logbookEntries:f["default"],logbookEntriesUpdated:p["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.register=o;var u=n(209),a=i(u),s=n(210),c=i(s),l=n(211),f=i(l),d=n(212),p=i(d),h=n(205),v=r(h),_=n(206),y=r(_),m=v;e.actions=m;var g=y;e.getters=g},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var u=function(){function t(t,e){for(var n=0;n1)for(var n=1;n \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 9637d5d2651..63e039a221a 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 9637d5d26516873b8a04a3c62b9596163c822a2d +Subproject commit 63e039a221ae6771e0d7c6990d9a93b7cc22fc64 From 9b964711822b82e1aa4db3d4f87702582cb974e5 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Wed, 16 Sep 2015 22:46:21 +0200 Subject: [PATCH 32/52] Fixed after param --- homeassistant/components/automation/time.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 821295fdffa..922b6b8287e 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -22,6 +22,10 @@ WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] def trigger(hass, config, action): """ Listen for state changes based on `config`. """ + hours = convert(config.get(CONF_HOURS), int) + minutes = convert(config.get(CONF_MINUTES), int) + seconds = convert(config.get(CONF_SECONDS), int) + if CONF_AFTER in config: after = dt_util.parse_time_str(config[CONF_AFTER]) if after is None: @@ -30,10 +34,6 @@ def trigger(hass, config, action): return False hours, minutes, seconds = after.hour, after.minute, after.second - hours = convert(config.get(CONF_HOURS), int) - minutes = convert(config.get(CONF_MINUTES), int) - seconds = convert(config.get(CONF_SECONDS), int) - def time_automation_listener(now): """ Listens for time changes and calls action. """ action() From 7e42b35b6237719ae0b92b882a0370229fbf6e96 Mon Sep 17 00:00:00 2001 From: Jeff Schroeder Date: Wed, 16 Sep 2015 22:57:22 -0500 Subject: [PATCH 33/52] Set logging of SQL queries to sqlite as debug log messages --- homeassistant/components/recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 73487163425..10f6576d23f 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -256,7 +256,7 @@ class Recorder(threading.Thread): """ Query the database. """ try: with self.conn, self.lock: - _LOGGER.info("Running query %s", sql_query) + _LOGGER.debug("Running query %s", sql_query) cur = self.conn.cursor() From 550f31d4c33edebc84ef6f28c96da81c4e8cd11d Mon Sep 17 00:00:00 2001 From: Jeff Schroeder Date: Wed, 16 Sep 2015 22:54:43 -0500 Subject: [PATCH 34/52] Quiet down some of the logging in the sonos platform This is due to the soco library logging very excessively and it using requests to connect to each Sonos speaker every 10 seconds (by default). This makes the logs much more pleasant to use for finding real issues. --- homeassistant/components/media_player/sonos.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index fd9e31e1810..faf4f6aa983 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -28,6 +28,14 @@ REQUIREMENTS = ['SoCo==0.11.1'] _LOGGER = logging.getLogger(__name__) +# The soco library is excessively chatty when it comes to logging and +# causes a LOT of spam in the logs due to making a http connection to each +# speaker every 10 seconds. Quiet it down a bit to just actual problems. +_SOCO_LOGGER = logging.getLogger('soco') +_SOCO_LOGGER.setLevel(logging.ERROR) +_REQUESTS_LOGGER = logging.getLogger('requests') +_REQUESTS_LOGGER.setLevel(logging.ERROR) + SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK From 4ad4d74ed4a572ed2762c70900bc65fd9a1ba530 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2015 23:12:38 -0700 Subject: [PATCH 35/52] Fix pip not detecting package installed --- homeassistant/util/package.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 5d32c087efe..966ecc1dcc2 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,6 +1,6 @@ """Helpers to install PyPi packages.""" -import os import logging +import os import pkg_resources import subprocess import sys @@ -15,25 +15,24 @@ def install_package(package, upgrade=True, target=None): """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successfull.""" # Not using 'import pip; pip.main([])' because it breaks the logger - args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] - - if upgrade: - args.append('--upgrade') - if target: - args += ['--target', os.path.abspath(target)] - with INSTALL_LOCK: if check_package_exists(package, target): return True _LOGGER.info('Attempting install of %s', package) + args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] + if upgrade: + args.append('--upgrade') + if target: + args += ['--target', os.path.abspath(target)] + try: return 0 == subprocess.call(args) except subprocess.SubprocessError: return False -def check_package_exists(package, target=None): +def check_package_exists(package, target): """Check if a package exists. Returns True when the requirement is met. Returns False when the package is not installed or doesn't meet req.""" @@ -43,16 +42,5 @@ def check_package_exists(package, target=None): # This is a zip file req = pkg_resources.Requirement.parse(urlparse(package).fragment) - if target: - work_set = pkg_resources.WorkingSet([target]) - search_fun = work_set.find - - else: - search_fun = pkg_resources.get_distribution - - try: - result = search_fun(req) - except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): - return False - - return bool(result) + return any(dist in req for dist in + pkg_resources.find_distributions(target)) From e68cc83e64865cffc5b957cbb6852ccb13480c0b Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Thu, 17 Sep 2015 08:24:06 +0200 Subject: [PATCH 36/52] return and output error if none of the 4 keys provided only parse hour/minute/second if after is not available --- homeassistant/components/automation/time.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 922b6b8287e..0f0e2fe38dd 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -19,20 +19,27 @@ CONF_WEEKDAY = "weekday" WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] +_LOGGER = logging.getLogger(__name__) + def trigger(hass, config, action): """ Listen for state changes based on `config`. """ - hours = convert(config.get(CONF_HOURS), int) - minutes = convert(config.get(CONF_MINUTES), int) - seconds = convert(config.get(CONF_SECONDS), int) - if CONF_AFTER in config: after = dt_util.parse_time_str(config[CONF_AFTER]) if after is None: - logging.getLogger(__name__).error( + _LOGGER.error( 'Received invalid after value: %s', config[CONF_AFTER]) return False hours, minutes, seconds = after.hour, after.minute, after.second + elif CONF_HOURS in config or CONF_MINUTES in config \ + or CONF_SECONDS in config: + hours = convert(config.get(CONF_HOURS), int) + minutes = convert(config.get(CONF_MINUTES), int) + seconds = convert(config.get(CONF_SECONDS), int) + else: + _LOGGER.error('One of %s, %s, %s OR %s needs to be specified', + CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER) + return False def time_automation_listener(now): """ Listens for time changes and calls action. """ From 8ec0c364575db5dc6397fd73742586f770617b4b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 Sep 2015 14:21:55 +0200 Subject: [PATCH 37/52] Fix return value --- homeassistant/components/sensor/openweathermap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 5ca292a599f..9d33264ea70 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -91,7 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error( "Connection error " "Please check your settings for OpenWeatherMap.") - return None + return False data = WeatherData(owm, forecast, hass.config.latitude, hass.config.longitude) From e90dbad37e03b164af23da16e4122b33e7130432 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 17 Sep 2015 08:34:10 +0200 Subject: [PATCH 38/52] Update docstrings --- homeassistant/components/camera/foscam.py | 46 +++++++++-------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 21f4589ca6c..78fd0f4d2e1 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -1,41 +1,31 @@ """ -Support for Foscam IP Cameras. - +homeassistant.components.camera.foscam +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This component provides basic support for Foscam IP cameras. As part of the basic support the following features will be provided: -MJPEG video streaming -To use this component, add the following to your config/configuration.yaml: +To use this component, add the following to your configuration.yaml file. camera: - platform: foscam - name: Door Camera - ip: 192.168.0.123 - port: 88 - username: visitor - password: password + platform: foscam + name: Door Camera + ip: 192.168.0.123 + port: 88 + username: YOUR_USERNAME + password: YOUR_PASSWORD -camera 2: - name: 'Second Camera' - ... -camera 3: - name: 'Camera Three' - ... - - -VARIABLES: - -These are the variables for the device_data array: +Variables: ip *Required -The IP address of your foscam device +The IP address of your Foscam device. username *Required -The username of a visitor or operator of your camera. -Oddly admin accounts don't seem to have access to take snapshots. +The username of a visitor or operator of your camera. Oddly admin accounts +don't seem to have access to take snapshots. password *Required @@ -49,6 +39,8 @@ port *Optional The port that the camera is running on. The default is 88. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.foscam.html """ import logging from homeassistant.helpers import validate_config @@ -72,9 +64,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-instance-attributes class FoscamCamera(Camera): - """ - An implementation of a Foscam IP camera. - """ + """ An implementation of a Foscam IP camera. """ def __init__(self, device_info): super(FoscamCamera, self).__init__() @@ -94,7 +84,7 @@ class FoscamCamera(Camera): self._name, self._snap_picture_url) def camera_image(self): - """ Return a still image reponse from the camera """ + """ Return a still image reponse from the camera. """ # send the request to snap a picture response = requests.get(self._snap_picture_url) @@ -111,5 +101,5 @@ class FoscamCamera(Camera): @property def name(self): - """ Return the name of this device """ + """ Return the name of this device. """ return self._name From ccecc0181d311ab8a5cec636ec87b26f4e53985e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 17 Sep 2015 08:34:26 +0200 Subject: [PATCH 39/52] Remove blank line --- homeassistant/components/sensor/glances.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 4a4d47c32f8..f6031b5a131 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -3,7 +3,6 @@ homeassistant.components.sensor.glances ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Gathers system information of hosts which running glances. - Configuration: To use the glances sensor you will need to add something like the following From 1a00d4a095ea367dc30f911c1344b07164ce5c65 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Thu, 17 Sep 2015 08:35:18 +0200 Subject: [PATCH 40/52] pylint fix --- homeassistant/components/automation/time.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 0f0e2fe38dd..9c60b766c6f 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -32,13 +32,13 @@ def trigger(hass, config, action): return False hours, minutes, seconds = after.hour, after.minute, after.second elif CONF_HOURS in config or CONF_MINUTES in config \ - or CONF_SECONDS in config: + or CONF_SECONDS in config: hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) else: _LOGGER.error('One of %s, %s, %s OR %s needs to be specified', - CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER) + CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER) return False def time_automation_listener(now): From 47af247d6a4a398f2a1ec903e0a01c865c53406d Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Thu, 17 Sep 2015 08:39:41 +0200 Subject: [PATCH 41/52] flake8 fix --- homeassistant/components/automation/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 9c60b766c6f..8679cac8667 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -32,7 +32,7 @@ def trigger(hass, config, action): return False hours, minutes, seconds = after.hour, after.minute, after.second elif CONF_HOURS in config or CONF_MINUTES in config \ - or CONF_SECONDS in config: + or CONF_SECONDS in config: hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) From 90e2aefd2346eab47890e43aaa04b4705580acb4 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Thu, 17 Sep 2015 08:55:17 +0200 Subject: [PATCH 42/52] flake8 fix --- homeassistant/components/automation/time.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 8679cac8667..17b0c989f16 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -31,8 +31,8 @@ def trigger(hass, config, action): 'Received invalid after value: %s', config[CONF_AFTER]) return False hours, minutes, seconds = after.hour, after.minute, after.second - elif CONF_HOURS in config or CONF_MINUTES in config \ - or CONF_SECONDS in config: + elif (CONF_HOURS in config or CONF_MINUTES in config + or CONF_SECONDS in config): hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) From d25a42426a056ada2729bfc450528681f5c8600b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 17 Sep 2015 03:25:36 -0400 Subject: [PATCH 43/52] add a way to restart on os x --- homeassistant/__main__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 428845031a5..120ca808860 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -103,6 +103,10 @@ def get_arguments(): '--uninstall-osx', action='store_true', help='Uninstalls from OS X.') + parser.add_argument( + '--restart-osx', + action='store_true', + help='Restarts on OS X.') if os.name != "nt": parser.add_argument( '--daemon', @@ -199,7 +203,6 @@ def uninstall_osx(): print("Home Assistant has been uninstalled.") - def main(): """ Starts Home Assistant. """ validate_python() @@ -216,6 +219,10 @@ def main(): if args.uninstall_osx: uninstall_osx() return + if args.restart_osx: + uninstall_osx() + install_osx() + return # daemon functions if args.pid_file: From 8c77418b6a2a2c8bef3ccddafbbc0734cb73ab2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 00:35:26 -0700 Subject: [PATCH 44/52] First pass for scripts to rule them all --- .travis.yml | 9 +-------- script/bootstrap | 4 ++++ script/bootstrap_frontend | 5 +++++ script/bootstrap_server | 10 ++++++++++ {scripts => script}/build_frontend | 6 +----- {scripts => script}/build_python_openzwave | 5 +---- script/cibuild | 3 +++ {scripts => script}/dev_docker | 5 +---- {scripts => script}/dev_openzwave_docker | 5 +---- {scripts => script}/get_entities.py | 0 {scripts => script}/hass-daemon | 0 script/lint | 9 +++++++++ script/server | 3 +++ script/setup | 4 ++++ scripts/run_tests => script/test | 9 +++++---- script/update | 4 ++++ scripts/check_style | 9 --------- scripts/update | 6 ------ 18 files changed, 52 insertions(+), 44 deletions(-) create mode 100755 script/bootstrap create mode 100755 script/bootstrap_frontend create mode 100644 script/bootstrap_server rename {scripts => script}/build_frontend (86%) rename {scripts => script}/build_python_openzwave (87%) create mode 100755 script/cibuild rename {scripts => script}/dev_docker (86%) rename {scripts => script}/dev_openzwave_docker (78%) rename {scripts => script}/get_entities.py (100%) rename {scripts => script}/hass-daemon (100%) mode change 100644 => 100755 create mode 100755 script/lint create mode 100755 script/server create mode 100755 script/setup rename scripts/run_tests => script/test (57%) create mode 100755 script/update delete mode 100755 scripts/check_style delete mode 100755 scripts/update diff --git a/.travis.yml b/.travis.yml index 339ed48d424..b7365b5aaec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,5 @@ sudo: false language: python python: - "3.4" -install: - - pip install -r requirements_all.txt - - pip install flake8 pylint coveralls script: - - flake8 homeassistant - - pylint homeassistant - - coverage run -m unittest discover tests -after_success: - - coveralls + - script/cibuild diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 00000000000..4de331c6a9d --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,4 @@ +cd "$(dirname "$0")/.." + +script/bootstrap_server +script/bootstrap_frontend diff --git a/script/bootstrap_frontend b/script/bootstrap_frontend new file mode 100755 index 00000000000..6fc94f95725 --- /dev/null +++ b/script/bootstrap_frontend @@ -0,0 +1,5 @@ +echo "Bootstrapping frontend..." +cd homeassistant/components/frontend/www_static/home-assistant-polymer +npm install +npm run setup_js_dev +cd ../../../../.. diff --git a/script/bootstrap_server b/script/bootstrap_server new file mode 100644 index 00000000000..431bec35497 --- /dev/null +++ b/script/bootstrap_server @@ -0,0 +1,10 @@ +cd "$(dirname "$0")/.." + +echo "Update the submodule to latest version..." +git submodule update + +echo "Installing dependencies..." +python3 -m pip install --upgrade -r requirements_all.txt + +echo "Installing development dependencies.." +python3 -m pip install --upgrade flake8 pylint coveralls py.test diff --git a/scripts/build_frontend b/script/build_frontend similarity index 86% rename from scripts/build_frontend rename to script/build_frontend index 9554e82256d..70eacdb6baf 100755 --- a/scripts/build_frontend +++ b/script/build_frontend @@ -1,12 +1,8 @@ # Builds the frontend for production -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." cd homeassistant/components/frontend/www_static/home-assistant-polymer -npm install npm run frontend_prod cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. diff --git a/scripts/build_python_openzwave b/script/build_python_openzwave similarity index 87% rename from scripts/build_python_openzwave rename to script/build_python_openzwave index 24bd8e2b64f..02c088fca44 100755 --- a/scripts/build_python_openzwave +++ b/script/build_python_openzwave @@ -3,10 +3,7 @@ # apt-get install cython3 libudev-dev python-sphinx python3-setuptools # pip3 install cython -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." if [ ! -d build ]; then mkdir build diff --git a/script/cibuild b/script/cibuild new file mode 100755 index 00000000000..6c8ddd602e5 --- /dev/null +++ b/script/cibuild @@ -0,0 +1,3 @@ +script/bootstrap_server +script/test coverage +coveralls diff --git a/scripts/dev_docker b/script/dev_docker similarity index 86% rename from scripts/dev_docker rename to script/dev_docker index b3672e56095..b63afaa36da 100755 --- a/scripts/dev_docker +++ b/script/dev_docker @@ -3,10 +3,7 @@ # Optional: pass in a timezone as first argument # If not given will attempt to mount /etc/localtime -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." docker build -t home-assistant-dev . diff --git a/scripts/dev_openzwave_docker b/script/dev_openzwave_docker similarity index 78% rename from scripts/dev_openzwave_docker rename to script/dev_openzwave_docker index f27816a8e39..387c38ef6da 100755 --- a/scripts/dev_openzwave_docker +++ b/script/dev_openzwave_docker @@ -1,10 +1,7 @@ # Open a docker that can be used to debug/dev python-openzwave # Pass in a command line argument to build -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." if [ $# -gt 0 ] then diff --git a/scripts/get_entities.py b/script/get_entities.py similarity index 100% rename from scripts/get_entities.py rename to script/get_entities.py diff --git a/scripts/hass-daemon b/script/hass-daemon old mode 100644 new mode 100755 similarity index 100% rename from scripts/hass-daemon rename to script/hass-daemon diff --git a/script/lint b/script/lint new file mode 100755 index 00000000000..120f364120f --- /dev/null +++ b/script/lint @@ -0,0 +1,9 @@ +# Run style checks + +cd "$(dirname "$0")/.." + +echo "Checking style with flake8..." +flake8 homeassistant + +echo "Checking style with pylint..." +pylint homeassistant diff --git a/script/server b/script/server new file mode 100755 index 00000000000..218944927fe --- /dev/null +++ b/script/server @@ -0,0 +1,3 @@ +cd "$(dirname "$0")/.." + +python3 -m homeassistant -c config diff --git a/script/setup b/script/setup new file mode 100755 index 00000000000..80c15646eaf --- /dev/null +++ b/script/setup @@ -0,0 +1,4 @@ +cd "$(dirname "$0")/.." + +git submodule init +script/bootstrap diff --git a/scripts/run_tests b/script/test similarity index 57% rename from scripts/run_tests rename to script/test index 75b25ca805a..28d88201806 100755 --- a/scripts/run_tests +++ b/script/test @@ -1,10 +1,11 @@ -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." + +echo "Running tests..." if [ "$1" = "coverage" ]; then coverage run -m unittest discover tests else python3 -m unittest discover tests fi + +script/lint diff --git a/script/update b/script/update new file mode 100755 index 00000000000..e0226ee9ca2 --- /dev/null +++ b/script/update @@ -0,0 +1,4 @@ +cd "$(dirname "$0")/.." + +git pull +git submodule update diff --git a/scripts/check_style b/scripts/check_style deleted file mode 100755 index 5fc8861b91a..00000000000 --- a/scripts/check_style +++ /dev/null @@ -1,9 +0,0 @@ -# Run style checks - -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi - -flake8 homeassistant -pylint homeassistant diff --git a/scripts/update b/scripts/update deleted file mode 100755 index be5e8fc01bf..00000000000 --- a/scripts/update +++ /dev/null @@ -1,6 +0,0 @@ -echo "The update script has been deprecated since Home Assistant v0.7" -echo -echo "Home Assistant is now distributed via PyPi and can be installed and" -echo "upgraded by running: pip3 install --upgrade homeassistant" -echo -echo "If you are developing a new feature for Home Assistant, run: git pull" From bf14067eb0c5437f012e9f21f2835042764a626f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 00:38:52 -0700 Subject: [PATCH 45/52] Add exec + doc header --- script/bootstrap | 5 +++++ script/cibuild | 5 +++++ script/server | 5 +++++ script/test | 5 +++++ script/update | 4 ++++ 5 files changed, 24 insertions(+) diff --git a/script/bootstrap b/script/bootstrap index 4de331c6a9d..f4cb6753fe8 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -1,3 +1,8 @@ +#!/bin/sh + +# script/bootstrap: Resolve all dependencies that the application requires to +# run. + cd "$(dirname "$0")/.." script/bootstrap_server diff --git a/script/cibuild b/script/cibuild index 6c8ddd602e5..20c0719a982 100755 --- a/script/cibuild +++ b/script/cibuild @@ -1,3 +1,8 @@ +#!/bin/sh + +# script/cibuild: Setup environment for CI to run tests. This is primarily +# designed to run on the continuous integration server. + script/bootstrap_server script/test coverage coveralls diff --git a/script/server b/script/server index 218944927fe..0904bfd728e 100755 --- a/script/server +++ b/script/server @@ -1,3 +1,8 @@ +#!/bin/sh + +# script/server: Launch the application and any extra required processes +# locally. + cd "$(dirname "$0")/.." python3 -m homeassistant -c config diff --git a/script/test b/script/test index 28d88201806..753ec340fd6 100755 --- a/script/test +++ b/script/test @@ -1,3 +1,8 @@ +#!/bin/sh + +# script/test: Run test suite for application. Optionallly pass in a path to an +# individual test file to run a single test. + cd "$(dirname "$0")/.." echo "Running tests..." diff --git a/script/update b/script/update index e0226ee9ca2..9f8b2530a7e 100755 --- a/script/update +++ b/script/update @@ -1,3 +1,7 @@ +#!/bin/sh + +# script/update: Update application to run for its current checkout. + cd "$(dirname "$0")/.." git pull From 95e05d4fc99a111298ccb717afa6b49814405478 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 00:42:15 -0700 Subject: [PATCH 46/52] Make script/bootstrap_server executable --- script/bootstrap_server | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/bootstrap_server diff --git a/script/bootstrap_server b/script/bootstrap_server old mode 100644 new mode 100755 From 049cd159ce97508b4c6a8266c9e2bb626165d00e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 00:44:22 -0700 Subject: [PATCH 47/52] Fix dev dependency pytest --- script/bootstrap_server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap_server b/script/bootstrap_server index 431bec35497..c68e198c014 100755 --- a/script/bootstrap_server +++ b/script/bootstrap_server @@ -7,4 +7,4 @@ echo "Installing dependencies..." python3 -m pip install --upgrade -r requirements_all.txt echo "Installing development dependencies.." -python3 -m pip install --upgrade flake8 pylint coveralls py.test +python3 -m pip install --upgrade flake8 pylint coveralls pytest From e0c1885a718f89105cd6773871d0645851793de5 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 17 Sep 2015 03:52:04 -0400 Subject: [PATCH 48/52] add blank line --- homeassistant/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 120ca808860..e97ed0c6386 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -203,6 +203,7 @@ def uninstall_osx(): print("Home Assistant has been uninstalled.") + def main(): """ Starts Home Assistant. """ validate_python() From 737d7c9d2279bba89de5955737b9cec509f91837 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 08:54:56 -0700 Subject: [PATCH 49/52] Add travis install section back --- .travis.yml | 2 ++ script/cibuild | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b7365b5aaec..4a4dfbc2354 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,7 @@ sudo: false language: python python: - "3.4" +install: + - script/bootstrap_server script: - script/cibuild diff --git a/script/cibuild b/script/cibuild index 20c0719a982..ade1b1d91c5 100755 --- a/script/cibuild +++ b/script/cibuild @@ -3,6 +3,5 @@ # script/cibuild: Setup environment for CI to run tests. This is primarily # designed to run on the continuous integration server. -script/bootstrap_server script/test coverage coveralls From 4b0c4168444947f825246fb004dd8bb0d56ae554 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 09:08:58 -0700 Subject: [PATCH 50/52] Use pytest for running tests --- script/bootstrap_server | 2 +- script/test | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/script/bootstrap_server b/script/bootstrap_server index c68e198c014..8d71e01fa78 100755 --- a/script/bootstrap_server +++ b/script/bootstrap_server @@ -7,4 +7,4 @@ echo "Installing dependencies..." python3 -m pip install --upgrade -r requirements_all.txt echo "Installing development dependencies.." -python3 -m pip install --upgrade flake8 pylint coveralls pytest +python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov diff --git a/script/test b/script/test index 753ec340fd6..56fe4dcec89 100755 --- a/script/test +++ b/script/test @@ -5,12 +5,12 @@ cd "$(dirname "$0")/.." +script/lint + echo "Running tests..." if [ "$1" = "coverage" ]; then - coverage run -m unittest discover tests + py.test --cov homeassistant tests else - python3 -m unittest discover tests + py.test tests fi - -script/lint From 4371355be1c054170adef7f6940360cc460504ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 23:12:55 -0700 Subject: [PATCH 51/52] Better errors on time automation trigger --- homeassistant/components/automation/time.py | 43 ++++++++++++--------- tests/components/automation/test_time.py | 32 +++++++++++++-- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 17b0c989f16..559832eee80 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -27,8 +27,7 @@ def trigger(hass, config, action): if CONF_AFTER in config: after = dt_util.parse_time_str(config[CONF_AFTER]) if after is None: - _LOGGER.error( - 'Received invalid after value: %s', config[CONF_AFTER]) + _error_time(config[CONF_AFTER], CONF_AFTER) return False hours, minutes, seconds = after.hour, after.minute, after.second elif (CONF_HOURS in config or CONF_MINUTES in config @@ -63,27 +62,27 @@ def if_action(hass, config): CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY) return None + if before is not None: + before = dt_util.parse_time_str(before) + if before is None: + _error_time(before, CONF_BEFORE) + return None + + if after is not None: + after = dt_util.parse_time_str(after) + if after is None: + _error_time(after, CONF_AFTER) + return None + def time_if(): """ Validate time based if-condition """ now = dt_util.now() - if before is not None: - time = dt_util.parse_time_str(before) - if time is None: + if before is not None and now > now.replace(hour=before.hour, + minute=before.minute): return False - before_point = now.replace(hour=time.hour, minute=time.minute) - - if now > before_point: - return False - - if after is not None: - time = dt_util.parse_time_str(after) - if time is None: - return False - - after_point = now.replace(hour=time.hour, minute=time.minute) - - if now < after_point: + if after is not None and now < now.replace(hour=after.hour, + minute=after.minute): return False if weekday is not None: @@ -96,3 +95,11 @@ def if_action(hass, config): return True return time_if + + +def _error_time(value, key): + """ Helper method to print error. """ + _LOGGER.error( + "Received invalid value for '%s': %s", key, value) + if isinstance(value, int): + _LOGGER.error('Make sure you wrap time values in quotes') diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index f7187592c66..95997bfec42 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -285,9 +285,9 @@ class TestAutomationTime(unittest.TestCase): automation.DOMAIN: { 'trigger': { 'platform': 'time', - 'hours': 0, - 'minutes': 0, - 'seconds': 0, + 'hours': 1, + 'minutes': 2, + 'seconds': 3, }, 'action': { 'execute_service': 'test.automation' @@ -296,7 +296,7 @@ class TestAutomationTime(unittest.TestCase): })) fire_time_changed(self.hass, dt_util.utcnow().replace( - hour=0, minute=0, second=0)) + hour=1, minute=2, second=3)) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -320,6 +320,30 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + @patch('homeassistant.components.automation.time._LOGGER.error') + def test_if_not_fires_using_wrong_after(self, mock_error): + """ YAML translates time values to total seconds. This should break the + before rule. """ + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'after': 3605, + # Total seconds. Hour = 3600 second + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace( + hour=1, minute=0, second=5)) + + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + self.assertEqual(2, mock_error.call_count) + def test_if_action_before(self): automation.setup(self.hass, { automation.DOMAIN: { From 6c1f44242c33af7eaa1fa7c369cd8f541f61cee3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Sep 2015 23:55:47 -0700 Subject: [PATCH 52/52] Update setup script --- script/setup | 1 + 1 file changed, 1 insertion(+) diff --git a/script/setup b/script/setup index 80c15646eaf..6d3a774dd54 100755 --- a/script/setup +++ b/script/setup @@ -2,3 +2,4 @@ cd "$(dirname "$0")/.." git submodule init script/bootstrap +python3 setup.py develop