From 0f904260231b3cf82d0788fd0026c04fccf37e64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Nov 2016 16:06:42 -0800 Subject: [PATCH 001/137] Version bump to 0.34.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index aa1f3654851..1b0921ccc95 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 33 -PATCH_VERSION = '0' +MINOR_VERSION = 34 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 123f4acfc151fb0c8ff94dca217fa5dc5fc03edb Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sun, 20 Nov 2016 20:49:54 +0100 Subject: [PATCH 002/137] ZWave lights: Not use super() (#4476) * Not use super * Review changes --- homeassistant/components/light/zwave.py | 2 +- homeassistant/components/zwave/__init__.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index d4e94b00e66..e1049192f51 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] customize = hass.data['zwave_customize'] - name = super().entity_id + name = '{}.{}'.format(DOMAIN, zwave.object_id(value)) node_config = customize.get(name, {}) refresh = node_config.get(zwave.CONF_REFRESH_VALUE) delay = node_config.get(zwave.CONF_REFRESH_DELAY) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a6294f560be..471b45feed0 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -193,19 +193,19 @@ def _node_object_id(node): return node_object_id -def _object_id(value): +def object_id(value): """Return the object_id of the device value. The object_id contains node_id and value instance id to not collide with other entity_ids. """ - object_id = "{}_{}_{}".format(slugify(_value_name(value)), - value.node.node_id, value.index) + _object_id = "{}_{}_{}".format(slugify(_value_name(value)), + value.node.node_id, value.index) # Add the instance id if there is more than one instance for the value if value.instance > 1: return '{}_{}'.format(object_id, value.instance) - return object_id + return _object_id def nice_print_node(node): @@ -341,7 +341,7 @@ def setup(hass, config): node.generic, node.specific, value.command_class, value.type, value.genre) - name = "{}.{}".format(component, _object_id(value)) + name = "{}.{}".format(component, object_id(value)) node_config = customize.get(name, {}) @@ -594,7 +594,7 @@ class ZWaveDeviceEntity: The object_id contains node_id and value instance id to not collide with other entity_ids. """ - return _object_id(self._value) + return object_id(self._value) @property def device_state_attributes(self): From b8e462cf5b711180c1543efd425040f394143652 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 21 Nov 2016 06:06:17 -0500 Subject: [PATCH 003/137] Bump pyvera to 0.2.21 pyvera 0.2.21 fixes the fact that use of requests.get was not using a timeout. Some times (after a few days of use) the pyvera poll loop would hang indefinitely on a requests.get of the event interface. This would cause the pyvera thread to hang completely. It would also prevent graceful shutdown, as pyvera does a thread join. The new version uses a timeout, so that we won't lock up any more. --- homeassistant/components/vera.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index c1cc5d7cbf7..75dd7428010 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -20,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.20'] +REQUIREMENTS = ['pyvera==0.2.21'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f8a5d4dac24..e193e9c7ea2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ python-wink==0.10.0 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.20 +pyvera==0.2.21 # homeassistant.components.notify.html5 pywebpush==0.6.1 From 40a21455588639918e18a3406d2cb1e461c84c3d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:25:43 +0100 Subject: [PATCH 004/137] Upgrade yahoo-finance to 1.4.0 (#4483) --- homeassistant/components/sensor/yahoo_finance.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/yahoo_finance.py b/homeassistant/components/sensor/yahoo_finance.py index 7316528849c..90b388433dd 100644 --- a/homeassistant/components/sensor/yahoo_finance.py +++ b/homeassistant/components/sensor/yahoo_finance.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['yahoo-finance==1.3.2'] +REQUIREMENTS = ['yahoo-finance==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e193e9c7ea2..302b5412522 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -572,7 +572,7 @@ xboxapi==0.1.1 xmltodict==0.10.2 # homeassistant.components.sensor.yahoo_finance -yahoo-finance==1.3.2 +yahoo-finance==1.4.0 # homeassistant.components.sensor.yweather yahooweather==0.8 From 63461e9007de617fad2e51d4b27a913ea68231d6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:27:15 +0100 Subject: [PATCH 005/137] Upgrade slacker to 0.9.30 (#4484) --- homeassistant/components/notify/slack.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 7ced616c9d2..2976b44d6e9 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_USERNAME, CONF_ICON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['slacker==0.9.29'] +REQUIREMENTS = ['slacker==0.9.30'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 302b5412522..1d4bdbfb06a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -486,7 +486,7 @@ scsgate==0.1.0 sendgrid==3.6.2 # homeassistant.components.notify.slack -slacker==0.9.29 +slacker==0.9.30 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 From ed1d0b419781f2b14d378b358aead8216085a940 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:27:48 +0100 Subject: [PATCH 006/137] Upgrade astral to 1.3.2 (#4505) --- homeassistant/components/sun.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 09cd4307443..8a98bc0c6df 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util as util -REQUIREMENTS = ['astral==1.3'] +REQUIREMENTS = ['astral==1.3.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1d4bdbfb06a..2d3df1d4c6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ apcaccess==0.0.4 apns2==0.1.1 # homeassistant.components.sun -astral==1.3 +astral==1.3.2 # homeassistant.components.sensor.linux_battery batinfo==0.4.2 From 7207c2cca15784c0427de77a2cf95b017af1d6fb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:28:31 +0100 Subject: [PATCH 007/137] Upgrade sendgrid to 3.6.3 (#4485) --- homeassistant/components/notify/sendgrid.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index 35e7d10cacb..54f0f4b8cb3 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==3.6.2'] +REQUIREMENTS = ['sendgrid==3.6.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2d3df1d4c6c..44aea12d3d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -483,7 +483,7 @@ schiene==0.18 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==3.6.2 +sendgrid==3.6.3 # homeassistant.components.notify.slack slacker==0.9.30 From 608b48290656be78ea136978ee5ec33263e5c6cf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:29:06 +0100 Subject: [PATCH 008/137] Upgrade sqlalchemy to 1.1.4 (#4486) --- homeassistant/components/recorder/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 6e8869e2d63..8de8925e093 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util DOMAIN = 'recorder' -REQUIREMENTS = ['sqlalchemy==1.1.3'] +REQUIREMENTS = ['sqlalchemy==1.1.4'] DEFAULT_URL = 'sqlite:///{hass_config_path}' DEFAULT_DB_FILE = 'home-assistant_v2.db' diff --git a/requirements_all.txt b/requirements_all.txt index 44aea12d3d5..b7da66e2049 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -505,7 +505,7 @@ speedtest-cli==0.3.4 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.1.3 +sqlalchemy==1.1.4 # homeassistant.components.statsd statsd==3.2.1 From eb8093934faeceb448f51fd67e7080f679f22a14 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:31:14 +0100 Subject: [PATCH 009/137] Upgrade python-hpilo to 3.9 (#4482) --- homeassistant/components/sensor/hp_ilo.py | 6 +++--- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py index d71e8f5ad6e..17a58dd9862 100644 --- a/homeassistant/components/sensor/hp_ilo.py +++ b/homeassistant/components/sensor/hp_ilo.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-hpilo==3.8'] +REQUIREMENTS = ['python-hpilo==3.9'] _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the HP ILO sensor.""" + """Set up the HP ILO sensor.""" hostname = config.get(CONF_HOST) port = config.get(CONF_PORT) login = config.get(CONF_USERNAME) @@ -83,7 +83,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HpIloSensor(Entity): - """Representation a HP ILO sensor.""" + """Representation of a HP ILO sensor.""" def __init__(self, hp_ilo_data, sensor_type, client_name): """Initialize the sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index b7da66e2049..def4a0c18cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ python-digitalocean==1.10.1 python-forecastio==1.3.5 # homeassistant.components.sensor.hp_ilo -python-hpilo==3.8 +python-hpilo==3.9 # homeassistant.components.lirc # python-lirc==1.2.3 From aed797f438ac507034d3857d506eb8b043dcb379 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Nov 2016 17:32:05 +0100 Subject: [PATCH 010/137] Upgrade freesms to 0.1.1 (#4491) --- homeassistant/components/notify/free_mobile.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/free_mobile.py b/homeassistant/components/notify/free_mobile.py index 06126e4fbc2..d8631fe6106 100644 --- a/homeassistant/components/notify/free_mobile.py +++ b/homeassistant/components/notify/free_mobile.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['freesms==0.1.0'] +REQUIREMENTS = ['freesms==0.1.1'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/requirements_all.txt b/requirements_all.txt index def4a0c18cb..184cf5b2072 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ fitbit==0.2.3 fixerio==0.1.1 # homeassistant.components.notify.free_mobile -freesms==0.1.0 +freesms==0.1.1 # homeassistant.components.conversation fuzzywuzzy==0.14.0 From 859d0d5ad616b934c9821cb62f9a42ff6b659602 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 22 Nov 2016 04:32:21 +0100 Subject: [PATCH 011/137] Bugfix device_tracker init tracker scan (#4514) --- homeassistant/components/device_tracker/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 7390357f4d7..b00a1044ad6 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -530,7 +530,7 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, else: host_name = scanner.get_device_name(mac) seen.add(mac) - hass.async_add_job(async_see_device(mac=mac, host_name=host_name)) + hass.add_job(async_see_device(mac=mac, host_name=host_name)) async_track_utc_time_change( hass, device_tracker_scan, second=range(0, 60, interval)) From 835577b2bc459195b0a75d375e6faa4c1a1c9de6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 22 Nov 2016 04:33:08 +0100 Subject: [PATCH 012/137] Bugfix discovery use wrong time async (#4515) * Bugfix discovery use wrong time async * fix lint --- homeassistant/helpers/discovery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 811b2aeeaf8..051d07b2435 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -48,7 +48,7 @@ def async_listen(hass, service, callback): def discover(hass, service, discovered=None, component=None, hass_config=None): """Fire discovery event. Can ensure a component is loaded.""" - hass.async_add_job( + hass.add_job( async_discover(hass, service, discovered, component, hass_config)) @@ -127,7 +127,7 @@ def load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. """ - hass.async_add_job( + hass.add_job( async_load_platform(hass, component, platform, discovered, hass_config)) From a73fbbaf7aab69165ffd2a44cb7750ff5aa671c3 Mon Sep 17 00:00:00 2001 From: hexa- Date: Tue, 22 Nov 2016 04:34:48 +0100 Subject: [PATCH 013/137] switch.tplink: expect daily stats to be empty (#4504) Signed-off-by: Martin Weinelt --- homeassistant/components/switch/tplink.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 0707ee3756b..bcc1b329fa8 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -97,8 +97,12 @@ class SmartPlugSwitch(SwitchDevice): = "%.1f A" % emeter_readings["current"] emeter_statics = self.smartplug.get_emeter_daily() - self._emeter_params[ATTR_DAILY_CONSUMPTION] \ - = "%.2f kW" % emeter_statics[int(time.strftime("%e"))] + try: + self._emeter_params[ATTR_DAILY_CONSUMPTION] \ + = "%.2f kW" % emeter_statics[int(time.strftime("%e"))] + except KeyError: + # device returned no daily history + pass except OSError: _LOGGER.warning('Could not update status for %s', self.name) From 1f573b46a45687ca2681342c55d9cc46f1e2bb65 Mon Sep 17 00:00:00 2001 From: Jack Chapple Date: Tue, 22 Nov 2016 03:35:36 +0000 Subject: [PATCH 014/137] Fixes #4500 (#4502) --- homeassistant/components/light/zwave.py | 2 +- homeassistant/components/zwave/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index e1049192f51..de471db4957 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -201,7 +201,7 @@ class ZwaveColorLight(ZwaveDimmer): self._rgb = None self._ct = None - super().__init__(value) + super().__init__(value, refresh, delay) # Create a listener so the color values can be linked to this entity dispatcher.connect( diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 471b45feed0..7c4d5e02879 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -204,7 +204,7 @@ def object_id(value): # Add the instance id if there is more than one instance for the value if value.instance > 1: - return '{}_{}'.format(object_id, value.instance) + return '{}_{}'.format(_object_id, value.instance) return _object_id From 248f5c02094ec0d9b59e3af39e320a78fc859392 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Tue, 22 Nov 2016 04:36:44 +0100 Subject: [PATCH 015/137] Neato Fixes (#4490) * Fix, switch state. Move constants to hub * Responsiveness * Whitespace * Delay was not needed as commands does not return until done. --- homeassistant/components/neato.py | 47 +++++++++++++++++++++++ homeassistant/components/sensor/neato.py | 49 +----------------------- homeassistant/components/switch/neato.py | 8 ++-- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 0c77c3a6b5c..e0b36721f74 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -31,6 +31,53 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +STATES = { + 1: 'Idle', + 2: 'Busy', + 3: 'Pause', + 4: 'Error' +} + +MODE = { + 1: 'Eco', + 2: 'Turbo' +} + +ACTION = { + 0: 'No action', + 1: 'House cleaning', + 2: 'Spot cleaning', + 3: 'Manual cleaning', + 4: 'Docking', + 5: 'User menu active', + 6: 'Cleaning cancelled', + 7: 'Updating...', + 8: 'Copying logs...', + 9: 'Calculating position...', + 10: 'IEC test' +} + +ERRORS = { + 'ui_error_brush_stuck': 'Brush stuck', + 'ui_error_brush_overloaded': 'Brush overloaded', + 'ui_error_bumper_stuck': 'Bumper stuck', + 'ui_error_dust_bin_missing': 'Dust bin missing', + 'ui_error_dust_bin_full': 'Dust bin full', + 'ui_error_dust_bin_emptied': 'Dust bin emptied', + 'ui_error_navigation_backdrop_leftbump': 'Clear my path', + 'ui_error_navigation_noprogress': 'Clear my path', + 'ui_error_navigation_origin_unclean': 'Clear my path', + 'ui_error_navigation_pathproblems_returninghome': 'Cannot return to base', + 'ui_error_navigation_falling': 'Clear my path', + 'ui_error_picked_up': 'Picked up', + 'ui_error_stuck': 'Stuck!' +} + +ALERTS = { + 'ui_alert_dust_bin_full': 'Please empty dust bin', + 'ui_alert_recovering_location': 'Returning to start' +} + def setup(hass, config): """Setup the Verisure component.""" diff --git a/homeassistant/components/sensor/neato.py b/homeassistant/components/sensor/neato.py index bbc0570740c..438e5fb189b 100644 --- a/homeassistant/components/sensor/neato.py +++ b/homeassistant/components/sensor/neato.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/sensor.neato/ import logging from homeassistant.helpers.entity import Entity -from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN +from homeassistant.components.neato import ( + NEATO_ROBOTS, NEATO_LOGIN, ACTION, ERRORS, MODE, ALERTS) _LOGGER = logging.getLogger(__name__) SENSOR_TYPE_STATUS = 'status' @@ -18,52 +19,6 @@ SENSOR_TYPES = { SENSOR_TYPE_BATTERY: ['Battery'] } -STATES = { - 1: 'Idle', - 2: 'Busy', - 3: 'Pause', - 4: 'Error' -} - -MODE = { - 1: 'Eco', - 2: 'Turbo' -} - -ACTION = { - 0: 'No action', - 1: 'House cleaning', - 2: 'Spot cleaning', - 3: 'Manual cleaning', - 4: 'Docking', - 5: 'User menu active', - 6: 'Cleaning cancelled', - 7: 'Updating...', - 8: 'Copying logs...', - 9: 'Calculating position...', - 10: 'IEC test' -} - -ERRORS = { - 'ui_error_brush_stuck': 'Brush stuck', - 'ui_error_brush_overloaded': 'Brush overloaded', - 'ui_error_bumper_stuck': 'Bumper stuck', - 'ui_error_dust_bin_missing': 'Dust bin missing', - 'ui_error_dust_bin_full': 'Dust bin full', - 'ui_error_dust_bin_emptied': 'Dust bin emptied', - 'ui_error_navigation_noprogress': 'Clear my path', - 'ui_error_navigation_origin_unclean': 'Clear my path', - 'ui_error_navigation_falling': 'Clear my path', - 'ui_error_picked_up': 'Picked up', - 'ui_error_stuck': 'Stuck!' - -} - -ALERTS = { - 'ui_alert_dust_bin_full': 'Please empty dust bin', - 'ui_alert_recovering_location': 'Returning to start' -} - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Neato sensor platform.""" diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index fdc5f9352b7..3b723acb748 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -4,7 +4,6 @@ Support for Neato Connected Vaccums switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.neato/ """ -import time import logging from homeassistant.const import STATE_OFF, STATE_ON @@ -57,17 +56,21 @@ class NeatoConnectedSwitch(ToggleEntity): self._state = self.robot.state _LOGGER.debug('self._state=%s', self._state) if self.type == SWITCH_TYPE_CLEAN: - if (self.robot.state['action'] == 1 and + if (self.robot.state['action'] == 1 or + self.robot.state['action'] == 2 or + self.robot.state['action'] == 3 and self.robot.state['state'] == 2): self._clean_state = STATE_ON else: self._clean_state = STATE_OFF + _LOGGER.debug('schedule_state=%s', self._schedule_state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug('self._state=%s', self._state) if self.robot.schedule_enabled: self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF + _LOGGER.debug('schedule_state=%s', self._schedule_state) @property def name(self): @@ -105,7 +108,6 @@ class NeatoConnectedSwitch(ToggleEntity): """Turn the switch off.""" if self.type == SWITCH_TYPE_CLEAN: self.robot.pause_cleaning() - time.sleep(1) self.robot.send_to_base() elif self.type == SWITCH_TYPE_SCHEDULE: self.robot.disable_schedule() From 6863d2e0afebee7f8279acddb3f30e4ca15b0b29 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 21 Nov 2016 20:39:23 -0700 Subject: [PATCH 016/137] Fixing 'Unknown' status for Nest Protect devices (#4475) * Fixing 'Unknown' status for Nest Protect devices * Fixing bad formatting --- homeassistant/components/sensor/nest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index ccf8be84adc..9f8e7396f93 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -195,6 +195,7 @@ class NestProtectSensor(NestSensor): if self.variable == 'battery_level': self._state = getattr(self.device, self.variable) else: + self._state = 'Unknown' if state == 0: self._state = 'Ok' if state == 1 or state == 2: @@ -202,8 +203,6 @@ class NestProtectSensor(NestSensor): if state == 3: self._state = 'Emergency' - self._state = 'Unknown' - @property def name(self): """Return the name of the nest, if any.""" From 86f3e2455d5d39726d81407ac3e9b3c62e385f09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Nov 2016 20:16:34 -0800 Subject: [PATCH 017/137] Skip google calendar offset test (#4520) --- tests/components/calendar/test_google.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 534faccd737..d19ecb65704 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -4,6 +4,8 @@ import logging import unittest from unittest.mock import patch +import pytest + import homeassistant.components.calendar as calendar_base import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util @@ -286,6 +288,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': '' }) + @pytest.mark.skip @patch('homeassistant.components.calendar.google.GoogleCalendarData') def test_all_day_offset_in_progress_event(self, mock_next_event): """Test that we can create an event trigger on device.""" From 547d93f631288c280a554825e4e6960b59e9cbc7 Mon Sep 17 00:00:00 2001 From: Gilles Margerie Date: Tue, 22 Nov 2016 07:45:17 +0000 Subject: [PATCH 018/137] Added source selection for Denon AVR Media Player (#4304) * Added source selection for Denon AVR Media Player * Update denon.py * Update denon.py * Update denon.py * Update denon.py * Update denon.py slight format update (space issue and new line) * Further update regarding formatting * Updated the source name with lowercase * Update denon.py --- .../components/media_player/denon.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) mode change 100644 => 100755 homeassistant/components/media_player/denon.py diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py old mode 100644 new mode 100755 index 96cf0b99462..351d6c8c491 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -10,8 +10,9 @@ import telnetlib import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) @@ -21,8 +22,9 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Music station' -SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ +SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -51,6 +53,8 @@ class DenonDevice(MediaPlayerDevice): self._host = host self._pwstate = 'PWSTANDBY' self._volume = 0 + self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER', + 'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'} self._muted = False self._mediasource = '' @@ -111,6 +115,11 @@ class DenonDevice(MediaPlayerDevice): """Boolean if volume is currently muted.""" return self._muted + @property + def source_list(self): + """List of available input sources.""" + return list(self._source_list.keys()) + @property def media_title(self): """Current media source.""" @@ -161,3 +170,7 @@ class DenonDevice(MediaPlayerDevice): def turn_on(self): """Turn the media player on.""" self.telnet_command('PWON') + + def select_source(self, source): + """Select input source.""" + self.telnet_command(self._source_list.get(source)) From 9cdcfae8f368aebec06569c47304423ce9c45a38 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 23 Nov 2016 01:36:29 +1100 Subject: [PATCH 019/137] New config parameter for min_max sensor to specify number of digits for rounding mean value (#4237) * new config parameter to specify number of digits for rounding average value * fixed two `line too long` errors * added three new tests for the mean sensor including test for precision of mean value --- homeassistant/components/sensor/min_max.py | 12 ++- tests/components/sensor/test_min_max.py | 93 +++++++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index b41966ac861..c1eb57170f4 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -32,6 +32,7 @@ ATTR_TO_PROPERTY = [ ] CONF_ENTITY_IDS = 'entity_ids' +CONF_ROUND_DIGITS = 'round_digits' DEFAULT_NAME = 'Min/Max/Avg Sensor' @@ -48,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.string, vol.In(SENSOR_TYPES.values())), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_ENTITY_IDS): cv.entity_ids, + vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int), }) @@ -57,20 +59,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): entity_ids = config.get(CONF_ENTITY_IDS) name = config.get(CONF_NAME) sensor_type = config.get(CONF_TYPE) + round_digits = config.get(CONF_ROUND_DIGITS) yield from async_add_devices( - [MinMaxSensor(hass, entity_ids, name, sensor_type)], True) + [MinMaxSensor(hass, entity_ids, name, sensor_type, round_digits)], + True) return True class MinMaxSensor(Entity): """Representation of a min/max sensor.""" - def __init__(self, hass, entity_ids, name, sensor_type): + def __init__(self, hass, entity_ids, name, sensor_type, round_digits): """Initialize the min/max sensor.""" self._hass = hass self._entity_ids = entity_ids self._sensor_type = sensor_type + self._round_digits = round_digits self._name = '{} {}'.format( name, next(v for k, v in SENSOR_TYPES.items() if self._sensor_type == v)) @@ -148,6 +153,7 @@ class MinMaxSensor(Entity): if len(sensor_values) == self.count_sensors: self.min_value = min(sensor_values) self.max_value = max(sensor_values) - self.mean = round(sum(sensor_values) / self.count_sensors, 2) + self.mean = round(sum(sensor_values) / self.count_sensors, + self._round_digits) else: self.min_value = self.max_value = self.mean = STATE_UNKNOWN diff --git a/tests/components/sensor/test_min_max.py b/tests/components/sensor/test_min_max.py index bf49d4113c4..11b08575f46 100644 --- a/tests/components/sensor/test_min_max.py +++ b/tests/components/sensor/test_min_max.py @@ -13,11 +13,13 @@ class TestMinMaxSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.values = [17, 20, 15.2] + self.values = [17, 20, 15.3] self.count = len(self.values) self.min = min(self.values) self.max = max(self.values) self.mean = round(sum(self.values) / self.count, 2) + self.mean_1_digit = round(sum(self.values) / self.count, 1) + self.mean_4_digits = round(sum(self.values) / self.count, 4) def teardown_method(self, method): """Stop everything that was started.""" @@ -81,6 +83,95 @@ class TestMinMaxSensor(unittest.TestCase): self.assertEqual(self.min, state.attributes.get('min_value')) self.assertEqual(self.mean, state.attributes.get('mean')) + def test_mean_sensor(self): + """Test the mean sensor.""" + config = { + 'sensor': { + 'platform': 'min_max', + 'name': 'test', + 'type': 'mean', + 'entity_ids': [ + 'sensor.test_1', + 'sensor.test_2', + 'sensor.test_3', + ] + } + } + + assert setup_component(self.hass, 'sensor', config) + + entity_ids = config['sensor']['entity_ids'] + + for entity_id, value in dict(zip(entity_ids, self.values)).items(): + self.hass.states.set(entity_id, value) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_mean') + + self.assertEqual(str(float(self.mean)), state.state) + self.assertEqual(self.min, state.attributes.get('min_value')) + self.assertEqual(self.max, state.attributes.get('max_value')) + + def test_mean_1_digit_sensor(self): + """Test the mean with 1-digit precision sensor.""" + config = { + 'sensor': { + 'platform': 'min_max', + 'name': 'test', + 'type': 'mean', + 'round_digits': 1, + 'entity_ids': [ + 'sensor.test_1', + 'sensor.test_2', + 'sensor.test_3', + ] + } + } + + assert setup_component(self.hass, 'sensor', config) + + entity_ids = config['sensor']['entity_ids'] + + for entity_id, value in dict(zip(entity_ids, self.values)).items(): + self.hass.states.set(entity_id, value) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_mean') + + self.assertEqual(str(float(self.mean_1_digit)), state.state) + self.assertEqual(self.min, state.attributes.get('min_value')) + self.assertEqual(self.max, state.attributes.get('max_value')) + + def test_mean_4_digit_sensor(self): + """Test the mean with 1-digit precision sensor.""" + config = { + 'sensor': { + 'platform': 'min_max', + 'name': 'test', + 'type': 'mean', + 'round_digits': 4, + 'entity_ids': [ + 'sensor.test_1', + 'sensor.test_2', + 'sensor.test_3', + ] + } + } + + assert setup_component(self.hass, 'sensor', config) + + entity_ids = config['sensor']['entity_ids'] + + for entity_id, value in dict(zip(entity_ids, self.values)).items(): + self.hass.states.set(entity_id, value) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_mean') + + self.assertEqual(str(float(self.mean_4_digits)), state.state) + self.assertEqual(self.min, state.attributes.get('min_value')) + self.assertEqual(self.max, state.attributes.get('max_value')) + def test_not_enough_sensor_value(self): """Test that there is nothing done if not enough values available.""" config = { From 5d18759146f90ab7be29e8d09897c7345cf65599 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 Nov 2016 15:41:37 +0100 Subject: [PATCH 020/137] Upgrade miflora to 0.1.13 (fixes #4479) (#4524) --- homeassistant/components/sensor/miflora.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 28906dfaef4..75042f4e911 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -16,7 +16,7 @@ from homeassistant.util import Throttle from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC) -REQUIREMENTS = ['miflora==0.1.12'] +REQUIREMENTS = ['miflora==0.1.13'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 184cf5b2072..6c40e9a63f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -275,7 +275,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.1.12 +miflora==0.1.13 # homeassistant.components.discovery netdisco==0.7.6 From c81735cc84c039e103a8ae6597005954c9fea32c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Nov 2016 08:21:08 -0800 Subject: [PATCH 021/137] Fix platform discovery when platform discovered during discovery of a (#4529) component --- homeassistant/helpers/discovery.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 051d07b2435..de16a0b907d 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -151,11 +151,10 @@ def async_load_platform(hass, component, platform, discovered=None, This method is a coroutine. """ did_lock = False - if component not in hass.config.components: - setup_lock = hass.data.get('setup_lock') - if setup_lock and setup_lock.locked(): - did_lock = True - yield from setup_lock.acquire() + setup_lock = hass.data.get('setup_lock') + if setup_lock and setup_lock.locked(): + did_lock = True + yield from setup_lock.acquire() setup_success = True From ce13b0989d59f5dfcdf43f22f55cc22cdaf79dd4 Mon Sep 17 00:00:00 2001 From: mnestor Date: Tue, 22 Nov 2016 13:15:39 -0500 Subject: [PATCH 022/137] Fix for #4520 (#4526) * Fix for #4520 * fix lint --- tests/components/calendar/test_google.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index d19ecb65704..5c49f3836b2 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -4,8 +4,6 @@ import logging import unittest from unittest.mock import patch -import pytest - import homeassistant.components.calendar as calendar_base import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util @@ -288,17 +286,15 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': '' }) - @pytest.mark.skip @patch('homeassistant.components.calendar.google.GoogleCalendarData') def test_all_day_offset_in_progress_event(self, mock_next_event): """Test that we can create an event trigger on device.""" tomorrow = dt_util.dt.date.today() \ + dt_util.dt.timedelta(days=1) - offset_hours = (25 - dt_util.now().hour) event_summary = 'Test All Day Event Offset In Progress' event = { - 'summary': '{} !!-{}:0'.format(event_summary, offset_hours), + 'summary': '{} !!-25:0'.format(event_summary), 'start': { 'date': tomorrow.isoformat() }, From 8e776b4dc0a245a2dc5ab77f3c7bede6363c5d8c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 22 Nov 2016 21:47:37 +0100 Subject: [PATCH 023/137] Fix wrong name handling in rfxtrx sensor (#4531) --- homeassistant/components/sensor/rfxtrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 663fd8899d3..5085dc942cc 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -124,7 +124,7 @@ class RfxtrxSensor(Entity): @property def name(self): """Get the name of the sensor.""" - return self._name + return "{} {}".format(self._name, self.data_type) @property def device_state_attributes(self): From 00019b9ff0fdc8d8ff211ac71e0649bc8f20d9e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Nov 2016 08:50:28 -0800 Subject: [PATCH 024/137] Fix warning in test --- tests/helpers/test_entity_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index a95c4a6d0fd..1e12d7c3ea3 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -292,7 +292,7 @@ class TestHelpersEntityComponent(unittest.TestCase): assert platform2_setup.called @patch('homeassistant.helpers.entity_component.EntityComponent' - '._async_setup_platform') + '._async_setup_platform', return_value=mock_coro()()) @patch('homeassistant.bootstrap.async_setup_component', return_value=mock_coro(True)()) def test_setup_does_discovery(self, mock_setup_component, mock_setup): From 2c7e895105529e3b3645cca6644e61ac9dedd10d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Nov 2016 17:38:04 -0800 Subject: [PATCH 025/137] Entity and climate: do not convert temperature unnecessary (#4522) * Climate: more consistent units * Prevent unnecessary conversion in entity component * int -> round * Disable Google tests because they connect to the internet * Remove default conversion rounding F->C * Add rounding of temp to weather comp * Fix equality * Maintain precision when converting temp in entity * Revert "Disable Google tests because they connect to the internet" This reverts commit b60485dc19bb97f4a502854d5ff2297330df0b40. --- homeassistant/components/climate/__init__.py | 11 ++++---- homeassistant/components/weather/__init__.py | 27 +++++++++++++++----- homeassistant/helpers/entity.py | 9 ++++--- homeassistant/util/temperature.py | 2 +- tests/util/test_unit_system.py | 3 ++- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 5ae10fca303..d35b142a8c4 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -562,16 +562,15 @@ class ClimateDevice(Entity): def _convert_for_display(self, temp): """Convert temperature into preferred units for display purposes.""" - if temp is None or not isinstance(temp, Number): + if (temp is None or not isinstance(temp, Number) or + self.temperature_unit == self.unit_of_measurement): return temp value = convert_temperature(temp, self.temperature_unit, self.unit_of_measurement) - if self.unit_of_measurement is TEMP_CELSIUS: - decimal_count = 1 + if self.unit_of_measurement == TEMP_CELSIUS: + return round(value, 1) else: # Users of fahrenheit generally expect integer units. - decimal_count = 0 - - return round(value, decimal_count) + return round(value) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 50173840657..26a5a41bf10 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -5,7 +5,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/weather/ """ import logging +from numbers import Number +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -84,10 +86,7 @@ class WeatherEntity(Entity): def state_attributes(self): """Return the state attributes.""" data = { - ATTR_WEATHER_TEMPERATURE: - convert_temperature( - self.temperature, self.temperature_unit, - self.hass.config.units.temperature_unit), + ATTR_WEATHER_TEMPERATURE: self._temp_for_display, ATTR_WEATHER_HUMIDITY: self.humidity, } @@ -124,6 +123,20 @@ class WeatherEntity(Entity): raise NotImplementedError() @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return None + def _temp_for_display(self): + """Convert temperature into preferred units for display purposes.""" + temp = self.temperature + unit = self.temperature_unit + hass_unit = self.hass.config.units.temperature_unit + + if (temp is None or not isinstance(temp, Number) or + unit == hass_unit): + return temp + + value = convert_temperature(temp, unit, hass_unit) + + if hass_unit == TEMP_CELSIUS: + return round(value, 1) + else: + # Users of fahrenheit generally expect integer units. + return round(value) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index b707f2f7199..87b05ded264 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -259,9 +259,12 @@ class Entity(object): # Convert temperature if we detect one try: unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) - if unit_of_measure in (TEMP_CELSIUS, TEMP_FAHRENHEIT): - units = self.hass.config.units - state = str(units.temperature(float(state), unit_of_measure)) + units = self.hass.config.units + if (unit_of_measure in (TEMP_CELSIUS, TEMP_FAHRENHEIT) and + unit_of_measure != units.temperature_unit): + prec = len(state) - state.index('.') - 1 if '.' in state else 0 + temp = units.temperature(float(state), unit_of_measure) + state = str(round(temp) if prec == 0 else round(temp, prec)) attr[ATTR_UNIT_OF_MEASUREMENT] = units.temperature_unit except ValueError: # Could not convert state to float diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index d6e245de04f..c773e564011 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -31,4 +31,4 @@ def convert(temperature: float, from_unit: str, to_unit: str) -> float: elif from_unit == TEMP_CELSIUS: return celsius_to_fahrenheit(temperature) else: - return round(fahrenheit_to_celsius(temperature), 1) + return fahrenheit_to_celsius(temperature) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index c99c2cf87bf..83edb056139 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -80,7 +80,8 @@ class TestUnitSystem(unittest.TestCase): METRIC_SYSTEM.temperature(25, METRIC_SYSTEM.temperature_unit)) self.assertEqual( 26.7, - METRIC_SYSTEM.temperature(80, IMPERIAL_SYSTEM.temperature_unit)) + round(METRIC_SYSTEM.temperature( + 80, IMPERIAL_SYSTEM.temperature_unit), 1)) def test_temperature_to_imperial(self): """Test temperature conversion to imperial system.""" From 962e5315aba94dcd2efbce8856ce6b9a2350bf54 Mon Sep 17 00:00:00 2001 From: mnestor Date: Tue, 22 Nov 2016 21:19:32 -0500 Subject: [PATCH 026/137] Mock call to google servers (#4532) * Fix for #4520 * mock call to do_auth to prevent call to google servers --- tests/components/calendar/test_google.py | 0 tests/components/test_google.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) mode change 100644 => 100755 tests/components/calendar/test_google.py diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py old mode 100644 new mode 100755 diff --git a/tests/components/test_google.py b/tests/components/test_google.py index aaaaf8a9983..10db913aa81 100644 --- a/tests/components/test_google.py +++ b/tests/components/test_google.py @@ -1,6 +1,7 @@ """The tests for the Google Calendar component.""" import logging import unittest +from unittest.mock import patch import homeassistant.components.google as google from homeassistant.bootstrap import setup_component @@ -20,7 +21,8 @@ class TestGoogle(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_setup_component(self): + @patch('homeassistant.components.google.do_authentication') + def test_setup_component(self, mock_do_auth): """Test setup component.""" config = { 'google': { From 4cc192e44508025b2ce8bfc2a92d5473114dcdf1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Nov 2016 18:34:48 -0800 Subject: [PATCH 027/137] Disable broken google offset test (#4540) --- tests/components/calendar/test_google.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 5c49f3836b2..014e52304c1 100755 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -4,6 +4,8 @@ import logging import unittest from unittest.mock import patch +import pytest + import homeassistant.components.calendar as calendar_base import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util @@ -286,6 +288,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'description': '' }) + @pytest.mark.skip @patch('homeassistant.components.calendar.google.GoogleCalendarData') def test_all_day_offset_in_progress_event(self, mock_next_event): """Test that we can create an event trigger on device.""" From b4756e6dda6379f78f71faf9dd681fda48554fae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Nov 2016 18:36:10 -0800 Subject: [PATCH 028/137] Bump netdisco (#4539) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 780f2ab75d5..142764ea522 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.7.6'] +REQUIREMENTS = ['netdisco==0.7.7'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index 6c40e9a63f8..80572e80016 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -278,7 +278,7 @@ mficlient==0.3.0 miflora==0.1.13 # homeassistant.components.discovery -netdisco==0.7.6 +netdisco==0.7.7 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 From 0827a26642db040b61d69bb2768f0604eae16427 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Wed, 23 Nov 2016 05:28:31 +0200 Subject: [PATCH 029/137] Yr.no update entities every hour (#4521) --- homeassistant/components/sensor/yr.py | 60 ++++++++++++++------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 3436288b627..6429c9dcaad 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -19,7 +19,8 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, async_track_utc_time_change) from homeassistant.util import dt as dt_util @@ -76,6 +77,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): yield from async_add_devices(dev) weather = YrData(hass, coordinates, dev) + # Update weather on the hour + async_track_utc_time_change(hass, weather.async_update, minute=0, second=0) yield from weather.async_update() @@ -139,40 +142,41 @@ class YrData(object): self.hass = hass @asyncio.coroutine - def async_update(self): + def async_update(self, *_): """Get the latest data from yr.no.""" def try_again(err: str): - """Schedule again later.""" + """Retry in 15 minutes.""" _LOGGER.warning('Retrying in 15 minutes: %s', err) + self._nextrun = None nxt = dt_util.utcnow() + timedelta(minutes=15) - async_track_point_in_utc_time(self.hass, self.async_update, nxt) + if nxt.minute >= 15: + async_track_point_in_utc_time(self.hass, self.async_update, + nxt) - try: - with async_timeout.timeout(10, loop=self.hass.loop): - resp = yield from self.hass.websession.get(self._url) - if resp.status != 200: - try_again('{} returned {}'.format(self._url, resp.status)) + if self._nextrun is None or dt_util.utcnow() >= self._nextrun: + try: + with async_timeout.timeout(10, loop=self.hass.loop): + resp = yield from self.hass.websession.get(self._url) + if resp.status != 200: + try_again('{} returned {}'.format(self._url, resp.status)) + return + text = yield from resp.text() + self.hass.async_add_job(resp.release()) + except (asyncio.TimeoutError, aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: + try_again(err) return - text = yield from resp.text() - self.hass.async_add_job(resp.release()) - except (asyncio.TimeoutError, aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as err: - try_again(err) - return - try: - import xmltodict - self.data = xmltodict.parse(text)['weatherdata'] - model = self.data['meta']['model'] - if '@nextrun' not in model: - model = model[0] - next_run = dt_util.parse_datetime(model['@nextrun']) - except (ExpatError, IndexError) as err: - try_again(err) - return - - # Schedule next execution - async_track_point_in_utc_time(self.hass, self.async_update, next_run) + try: + import xmltodict + self.data = xmltodict.parse(text)['weatherdata'] + model = self.data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + self._nextrun = dt_util.parse_datetime(model['@nextrun']) + except (ExpatError, IndexError) as err: + try_again(err) + return now = dt_util.utcnow() From 65b85ec6c002e8a4538868d484f1776b9f7568a5 Mon Sep 17 00:00:00 2001 From: Aaron Morris Date: Tue, 22 Nov 2016 23:45:06 -0500 Subject: [PATCH 030/137] Fix missing space in error message between "accuracy" and "is" (#4542) --- homeassistant/components/device_tracker/owntracks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index d73cbfdb2ac..44ae0359ff6 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -147,7 +147,7 @@ def setup_scanner(hass, config, see): data_type, max_gps_accuracy, payload) return None if convert(data.get('acc'), float, 1.0) == 0.0: - _LOGGER.warning('Ignoring %s update because GPS accuracy' + _LOGGER.warning('Ignoring %s update because GPS accuracy ' 'is zero: %s', data_type, payload) return None From 1d8a1df2c497912262ac7a2fa03e10294c3b959c Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Wed, 23 Nov 2016 06:48:22 +0100 Subject: [PATCH 031/137] Refactor tellstick code (#4460) * Refactor tellstick code for increased readability. Especially highlight if "device" is a telldus core device or a HA entity. * Refactor Tellstick object model for increased clarity. * Update comments. Unify better with sensors. Fix typo bug. Add debug logging. * Refactor tellstick code for increased readability. Especially highlight if "device" is a telldus core device or a HA entity. * Refactor Tellstick object model for increased clarity. * Update comments. Unify better with sensors. Fix typo bug. Add debug logging. * Fix lint issues. --- homeassistant/components/light/tellstick.py | 84 ++++---- homeassistant/components/sensor/tellstick.py | 29 +-- homeassistant/components/switch/tellstick.py | 62 +++--- homeassistant/components/tellstick.py | 204 ++++++++++++------- 4 files changed, 204 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 3f9364a4cd5..9afc826e83c 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -6,14 +6,14 @@ https://home-assistant.io/components/light.tellstick/ """ import voluptuous as vol -from homeassistant.components import tellstick from homeassistant.components.light import (ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, - ATTR_DISCOVER_CONFIG) + ATTR_DISCOVER_CONFIG, + DOMAIN, TellstickDevice) -PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN}) +PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN}) SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS @@ -22,32 +22,25 @@ SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick lights.""" if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None or - tellstick.TELLCORE_REGISTRY is None): + discovery_info[ATTR_DISCOVER_DEVICES] is None): return + # Allow platform level override, fallback to module config signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS) - add_devices(TellstickLight( - tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) - for switch_id in discovery_info[ATTR_DISCOVER_DEVICES]) + add_devices(TellstickLight(tellcore_id, signal_repetitions) + for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]) -class TellstickLight(tellstick.TellstickDevice, Light): +class TellstickLight(TellstickDevice, Light): """Representation of a Tellstick light.""" - def __init__(self, tellstick_device, signal_repetitions): + def __init__(self, tellcore_id, signal_repetitions): """Initialize the light.""" - self._brightness = 255 - tellstick.TellstickDevice.__init__(self, - tellstick_device, - signal_repetitions) + super().__init__(tellcore_id, signal_repetitions) - @property - def is_on(self): - """Return true if switch is on.""" - return self._state + self._brightness = 255 @property def brightness(self): @@ -59,37 +52,32 @@ class TellstickLight(tellstick.TellstickDevice, Light): """Flag supported features.""" return SUPPORT_TELLSTICK - def set_tellstick_state(self, last_command_sent, last_data_sent): - """Update the internal representation of the switch.""" - from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_DIM - if last_command_sent == TELLSTICK_DIM: - if last_data_sent is not None: - self._brightness = int(last_data_sent) - self._state = self._brightness > 0 + def _parse_ha_data(self, kwargs): + """Turn the value from HA into something useful.""" + return kwargs.get(ATTR_BRIGHTNESS) + + def _parse_tellcore_data(self, tellcore_data): + """Turn the value recieved from tellcore into something useful.""" + if tellcore_data is not None: + brightness = int(tellcore_data) + return brightness else: - self._state = last_command_sent == TELLSTICK_TURNON + return None - def _send_tellstick_command(self, command, data): - """Handle the turn_on / turn_off commands.""" - from tellcore.constants import (TELLSTICK_TURNOFF, TELLSTICK_DIM) - if command == TELLSTICK_TURNOFF: - self.tellstick_device.turn_off() - elif command == TELLSTICK_DIM: - self.tellstick_device.dim(self._brightness) + def _update_model(self, new_state, data): + """Update the device entity state to match the arguments.""" + if new_state: + brightness = data + if brightness is not None: + self._brightness = brightness + + self._state = (self._brightness > 0) else: - raise NotImplementedError( - "Command not implemented: {}".format(command)) + self._state = False - def turn_on(self, **kwargs): - """Turn the switch on.""" - from tellcore.constants import TELLSTICK_DIM - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness is not None: - self._brightness = brightness - - self.call_tellstick(TELLSTICK_DIM, self._brightness) - - def turn_off(self, **kwargs): - """Turn the switch off.""" - from tellcore.constants import TELLSTICK_TURNOFF - self.call_tellstick(TELLSTICK_TURNOFF) + def _send_tellstick_command(self): + """Let tellcore update the device to match the current state.""" + if self._state: + self._tellcore_device.dim(self._brightness) + else: + self._tellcore_device.turn_off() diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 08e15cd332f..d16bb28f4f4 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -16,6 +16,8 @@ import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['tellcore-py==1.1.2'] +_LOGGER = logging.getLogger(__name__) + DatatypeDescription = namedtuple('DatatypeDescription', ['name', 'unit']) CONF_DATATYPE_MASK = 'datatype_mask' @@ -65,27 +67,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } try: - core = telldus.TelldusCore() + tellcore_lib = telldus.TelldusCore() except OSError: - logging.getLogger(__name__).exception('Could not initialize Tellstick') + _LOGGER.exception('Could not initialize Tellstick') return sensors = [] datatype_mask = config.get(CONF_DATATYPE_MASK) - for ts_sensor in core.sensors(): + for tellcore_sensor in tellcore_lib.sensors(): try: - sensor_name = config[ts_sensor.id] + sensor_name = config[tellcore_sensor.id] except KeyError: if config.get(CONF_ONLY_NAMED): continue - sensor_name = str(ts_sensor.id) + sensor_name = str(tellcore_sensor.id) for datatype in sensor_value_descriptions: - if datatype & datatype_mask and ts_sensor.has_value(datatype): - sensor_info = sensor_value_descriptions[datatype] - sensors.append(TellstickSensor( - sensor_name, ts_sensor, datatype, sensor_info)) + if datatype & datatype_mask: + if tellcore_sensor.has_value(datatype): + sensor_info = sensor_value_descriptions[datatype] + sensors.append(TellstickSensor( + sensor_name, tellcore_sensor, datatype, sensor_info)) add_devices(sensors) @@ -93,10 +96,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class TellstickSensor(Entity): """Representation of a Tellstick sensor.""" - def __init__(self, name, sensor, datatype, sensor_info): + def __init__(self, name, tellcore_sensor, datatype, sensor_info): """Initialize the sensor.""" - self.datatype = datatype - self.sensor = sensor + self._datatype = datatype + self._tellcore_sensor = tellcore_sensor self._unit_of_measurement = sensor_info.unit or None self._value = None @@ -119,4 +122,4 @@ class TellstickSensor(Entity): def update(self): """Update tellstick sensor.""" - self._value = self.sensor.value(self.datatype).value + self._value = self._tellcore_sensor.value(self._datatype).value diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index e5134c07a34..d3660ab36ca 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -6,61 +6,51 @@ https://home-assistant.io/components/switch.tellstick/ """ import voluptuous as vol -from homeassistant.components import tellstick -from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES, - ATTR_DISCOVER_CONFIG) +from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVER_CONFIG, + DOMAIN, TellstickDevice) from homeassistant.helpers.entity import ToggleEntity -PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN}) +PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick switches.""" if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None or - tellstick.TELLCORE_REGISTRY is None): + discovery_info[ATTR_DISCOVER_DEVICES] is None): return # Allow platform level override, fallback to module config - signal_repetitions = discovery_info.get( - ATTR_DISCOVER_CONFIG, tellstick.DEFAULT_SIGNAL_REPETITIONS) + signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, + DEFAULT_SIGNAL_REPETITIONS) - add_devices(TellstickSwitchDevice( - tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) - for switch_id in discovery_info[ATTR_DISCOVER_DEVICES]) + add_devices(TellstickSwitch(tellcore_id, signal_repetitions) + for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]) -class TellstickSwitchDevice(tellstick.TellstickDevice, ToggleEntity): +class TellstickSwitch(TellstickDevice, ToggleEntity): """Representation of a Tellstick switch.""" - @property - def is_on(self): - """Return true if switch is on.""" - return self._state + def _parse_ha_data(self, kwargs): + """Turn the value from HA into something useful.""" + return None - def set_tellstick_state(self, last_command_sent, last_data_sent): - """Update the internal representation of the switch.""" - from tellcore.constants import TELLSTICK_TURNON - self._state = last_command_sent == TELLSTICK_TURNON + def _parse_tellcore_data(self, tellcore_data): + """Turn the value recieved from tellcore into something useful.""" + return None - def _send_tellstick_command(self, command, data): - """Handle the turn_on / turn_off commands.""" - from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_TURNOFF - if command == TELLSTICK_TURNON: - self.tellstick_device.turn_on() - elif command == TELLSTICK_TURNOFF: - self.tellstick_device.turn_off() + def _update_model(self, new_state, data): + """Update the device entity state to match the arguments.""" + self._state = new_state - def turn_on(self, **kwargs): - """Turn the switch on.""" - from tellcore.constants import TELLSTICK_TURNON - self.call_tellstick(TELLSTICK_TURNON) - - def turn_off(self, **kwargs): - """Turn the switch off.""" - from tellcore.constants import TELLSTICK_TURNOFF - self.call_tellstick(TELLSTICK_TURNOFF) + def _send_tellstick_command(self): + """Let tellcore update the device to match the current state.""" + if self._state: + self._tellcore_device.turn_on() + else: + self._tellcore_device.turn_off() @property def force_update(self) -> bool: diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index 6d8ad967ad2..cbd5ff20583 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -25,12 +25,12 @@ DEFAULT_SIGNAL_REPETITIONS = 1 ATTR_DISCOVER_DEVICES = 'devices' ATTR_DISCOVER_CONFIG = 'config' -# Use a global tellstick domain lock to handle Tellcore errors then calling -# to concurrently +# Use a global tellstick domain lock to avoid getting Tellcore errors when +# calling concurrently. TELLSTICK_LOCK = threading.Lock() -# Keep a reference the the callback registry. Used from entities that register -# callback listeners +# A TellstickRegistry that keeps a map from tellcore_id to the corresponding +# tellcore_device and HA device (entity). TELLCORE_REGISTRY = None CONFIG_SCHEMA = vol.Schema({ @@ -41,50 +41,52 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def _discover(hass, config, found_devices, component_name): +def _discover(hass, config, component_name, found_tellcore_devices): """Setup and send the discovery event.""" - if not len(found_devices): + if not len(found_tellcore_devices): return - _LOGGER.info( - "Discovered %d new %s devices", len(found_devices), component_name) + _LOGGER.info("Discovered %d new %s devices", len(found_tellcore_devices), + component_name) signal_repetitions = config[DOMAIN].get(ATTR_SIGNAL_REPETITIONS) discovery.load_platform(hass, component_name, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices, + ATTR_DISCOVER_DEVICES: found_tellcore_devices, ATTR_DISCOVER_CONFIG: signal_repetitions}, config) def setup(hass, config): """Setup the Tellstick component.""" - # pylint: disable=global-statement, import-error + from tellcore.constants import TELLSTICK_DIM + from tellcore.library import DirectCallbackDispatcher + from tellcore.telldus import TelldusCore + global TELLCORE_REGISTRY - import tellcore.telldus as telldus - import tellcore.constants as tellcore_constants - from tellcore.library import DirectCallbackDispatcher + try: + tellcore_lib = TelldusCore( + callback_dispatcher=DirectCallbackDispatcher()) + except OSError: + _LOGGER.exception('Could not initialize Tellstick') + return False - core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) - - TELLCORE_REGISTRY = TellstickRegistry(hass, core) - - devices = core.devices() + # Get all devices, switches and lights alike + all_tellcore_devices = tellcore_lib.devices() # Register devices - TELLCORE_REGISTRY.register_devices(devices) + TELLCORE_REGISTRY = TellstickRegistry(hass, tellcore_lib) + TELLCORE_REGISTRY.register_tellcore_devices(all_tellcore_devices) # Discover the switches - _discover(hass, config, [switch.id for switch in - devices if not switch.methods( - tellcore_constants.TELLSTICK_DIM)], - 'switch') + _discover(hass, config, 'switch', + [tellcore_device.id for tellcore_device in all_tellcore_devices + if not tellcore_device.methods(TELLSTICK_DIM)]) # Discover the lights - _discover(hass, config, [light.id for light in - devices if light.methods( - tellcore_constants.TELLSTICK_DIM)], - 'light') + _discover(hass, config, 'light', + [tellcore_device.id for tellcore_device in all_tellcore_devices + if tellcore_device.methods(TELLSTICK_DIM)]) return True @@ -92,50 +94,57 @@ def setup(hass, config): class TellstickRegistry(object): """Handle everything around Tellstick callbacks. - Keeps a map device ids to home-assistant entities. - Also responsible for registering / cleanup of callbacks. + Keeps a map device ids to the tellcore device object, and + another to the HA device objects (entities). + + Also responsible for registering / cleanup of callbacks, and for + dispatching the callbacks to the corresponding HA device object. All device specific logic should be elsewhere (Entities). """ def __init__(self, hass, tellcore_lib): """Initialize the Tellstick mappings and callbacks.""" - self._core_lib = tellcore_lib # used when map callback device id to ha entities. - self._id_to_entity_map = {} - self._id_to_device_map = {} - self._setup_device_callback(hass, tellcore_lib) + self._id_to_ha_device_map = {} + self._id_to_tellcore_device_map = {} + self._setup_tellcore_callback(hass, tellcore_lib) - def _device_callback(self, tellstick_id, method, data, cid): + def _tellcore_event_callback(self, tellcore_id, tellcore_command, + tellcore_data, cid): """Handle the actual callback from Tellcore.""" - entity = self._id_to_entity_map.get(tellstick_id, None) - if entity is not None: - entity.set_tellstick_state(method, data) - entity.schedule_update_ha_state() + ha_device = self._id_to_ha_device_map.get(tellcore_id, None) + if ha_device is not None: + # Pass it on to the HA device object + ha_device.update_from_tellcore(tellcore_command, tellcore_data) + ha_device.schedule_update_ha_state() - def _setup_device_callback(self, hass, tellcore_lib): + def _setup_tellcore_callback(self, hass, tellcore_lib): """Register the callback handler.""" - callback_id = tellcore_lib.register_device_event(self._device_callback) + callback_id = tellcore_lib.register_device_event( + self._tellcore_event_callback) def clean_up_callback(event): """Unregister the callback bindings.""" if callback_id is not None: tellcore_lib.unregister_callback(callback_id) + _LOGGER.debug("Tellstick callback unregistered") hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, clean_up_callback) - def register_entity(self, tellcore_id, entity): - """Register a new entity to receive callback updates.""" - self._id_to_entity_map[tellcore_id] = entity + def register_ha_device(self, tellcore_id, ha_device): + """Register a new HA device to receive callback updates.""" + self._id_to_ha_device_map[tellcore_id] = ha_device - def register_devices(self, devices): + def register_tellcore_devices(self, tellcore_devices): """Register a list of devices.""" - self._id_to_device_map.update( - {device.id: device for device in devices}) + self._id_to_tellcore_device_map.update( + {tellcore_device.id: tellcore_device for tellcore_device + in tellcore_devices}) - def get_device(self, tellcore_id): + def get_tellcore_device(self, tellcore_id): """Return a device by tellcore_id.""" - return self._id_to_device_map.get(tellcore_id, None) + return self._id_to_tellcore_device_map.get(tellcore_id, None) class TellstickDevice(Entity): @@ -144,19 +153,21 @@ class TellstickDevice(Entity): Contains the common logic for all Tellstick devices. """ - def __init__(self, tellstick_device, signal_repetitions): + def __init__(self, tellcore_id, signal_repetitions): """Initalize the Tellstick device.""" - self.signal_repetitions = signal_repetitions + self._signal_repetitions = signal_repetitions self._state = None - self.tellstick_device = tellstick_device - # Add to id to entity mapping - TELLCORE_REGISTRY.register_entity(tellstick_device.id, self) + # Look up our corresponding tellcore device + self._tellcore_device = TELLCORE_REGISTRY.get_tellcore_device( + tellcore_id) # Query tellcore for the current state self.update() + # Add ourselves to the mapping + TELLCORE_REGISTRY.register_ha_device(tellcore_id, self) @property def should_poll(self): - """Tell Home Assistant not to poll this entity.""" + """Tell Home Assistant not to poll this device.""" return False @property @@ -166,43 +177,80 @@ class TellstickDevice(Entity): @property def name(self): - """Return the name of the switch if any.""" - return self.tellstick_device.name + """Return the name of the device as reported by tellcore.""" + return self._tellcore_device.name - def set_tellstick_state(self, last_command_sent, last_data_sent): - """Set the private switch state.""" - raise NotImplementedError( - "set_tellstick_state needs to be implemented.") + @property + def is_on(self): + """Return true if the device is on.""" + return self._state - def _send_tellstick_command(self, command, data): - """Do the actual call to the tellstick device.""" - raise NotImplementedError( - "_call_tellstick needs to be implemented.") + def _parse_ha_data(self, kwargs): + """Turn the value from HA into something useful.""" + raise NotImplementedError - def call_tellstick(self, command, data=None): - """Send a command to the device.""" + def _parse_tellcore_data(self, tellcore_data): + """Turn the value recieved from tellcore into something useful.""" + raise NotImplementedError + + def _update_model(self, new_state, data): + """Update the device entity state to match the arguments.""" + raise NotImplementedError + + def _send_tellstick_command(self): + """Let tellcore update the device to match the current state.""" + raise NotImplementedError + + def _do_action(self, new_state, data): + """The logic for actually turning on or off the device.""" from tellcore.library import TelldusError + with TELLSTICK_LOCK: + # Update self with requested new state + self._update_model(new_state, data) + # ... and then send this new state to the Tellstick try: - for _ in range(self.signal_repetitions): - self._send_tellstick_command(command, data) - # Update the internal state - self.set_tellstick_state(command, data) - self.update_ha_state() + for _ in range(self._signal_repetitions): + self._send_tellstick_command() except TelldusError: _LOGGER.error(TelldusError) + self.update_ha_state() + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._do_action(True, self._parse_ha_data(kwargs)) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._do_action(False, None) + + def update_from_tellcore(self, tellcore_command, tellcore_data): + """Handle updates from the tellcore callback.""" + from tellcore.constants import (TELLSTICK_TURNON, TELLSTICK_TURNOFF, + TELLSTICK_DIM) + + if tellcore_command not in [TELLSTICK_TURNON, TELLSTICK_TURNOFF, + TELLSTICK_DIM]: + _LOGGER.debug("Unhandled tellstick command: %d", + tellcore_command) + return + + self._update_model(tellcore_command != TELLSTICK_TURNOFF, + self._parse_tellcore_data(tellcore_data)) def update(self): """Poll the current state of the device.""" - import tellcore.constants as tellcore_constants from tellcore.library import TelldusError + from tellcore.constants import (TELLSTICK_TURNON, TELLSTICK_TURNOFF, + TELLSTICK_DIM) + try: - last_command = self.tellstick_device.last_sent_command( - tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF | - tellcore_constants.TELLSTICK_DIM + last_tellcore_command = self._tellcore_device.last_sent_command( + TELLSTICK_TURNON | TELLSTICK_TURNOFF | TELLSTICK_DIM ) - last_value = self.tellstick_device.last_sent_value() - self.set_tellstick_state(last_command, last_value) + last_tellcore_data = self._tellcore_device.last_sent_value() + + self.update_from_tellcore(last_tellcore_command, + last_tellcore_data) except TelldusError: _LOGGER.error(TelldusError) From 0c47434aad46ee4a313f846968b2ad429f97f19b Mon Sep 17 00:00:00 2001 From: Thomas Friedel Date: Wed, 23 Nov 2016 07:10:45 +0100 Subject: [PATCH 032/137] Change Osram to use Github lightify dep (#4256) * used MindrustUK's version ( https://github.com/MindrustUK/python-lightify/commits/master/osramlightify.py ) from Oct 2, 2016 and changed the REQUIRMENTS line to use the fixed lightify component with thread safety fixes * reformatted long lines * updated osramlightify requirements in requirements_all.txt * ran script gen_requirements_all.py * rerun requirements gen script on linux * fixed some inspection warnings * zip file points to a specific commit * no requests to lights in properties, instead instance variables are update in update method * regenerated requirements_all.txt * removed call to update from is_on() property --- .../components/light/osramlightify.py | 108 +++++++++++++----- requirements_all.txt | 6 +- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index cdcaef8eb50..55d96236f2d 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -19,7 +19,8 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lightify==1.0.3'] +REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/' + 'd6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4'] _LOGGER = logging.getLogger(__name__) @@ -92,37 +93,43 @@ class OsramLightifyLight(Light): self._light = light self._light_id = light_id self.update_lights = update_lights + self._brightness = 0 + self._rgb = (0, 0, 0) + self._name = "" + self._temperature = TEMP_MIN + self._state = False + self.update() @property def name(self): """Return the name of the device if any.""" - return self._light.name() + return self._name @property def rgb_color(self): """Last RGB color value set.""" - return self._light.rgb() + _LOGGER.debug("rgb_color light state for light: %s is: %s %s %s ", + self._name, self._rgb[0], self._rgb[1], self._rgb[2]) + return self._rgb @property def color_temp(self): """Return the color temperature.""" - o_temp = self._light.temp() - temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) * - (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN)) - return temperature + return self._temperature @property def brightness(self): """Brightness of this light between 0..255.""" - return int(self._light.lum() * 2.55) + _LOGGER.debug("brightness for light %s is: %s", + self._name, self._brightness) + return self._brightness @property def is_on(self): """Update Status to True if device is on.""" - self.update_lights() _LOGGER.debug("is_on light state for light: %s is: %s", - self._light.name(), self._light.on()) - return self._light.on() + self._name, self._state) + return self._state @property def supported_features(self): @@ -131,47 +138,86 @@ class OsramLightifyLight(Light): def turn_on(self, **kwargs): """Turn the device on.""" - brightness = 100 - if self.brightness: - brightness = int(self.brightness / 2.55) + _LOGGER.debug("turn_on Attempting to turn on light: %s ", + self._name) + + self._light.set_onoff(1) + self._state = self._light.on() if ATTR_TRANSITION in kwargs: - fade = kwargs[ATTR_TRANSITION] * 10 + transition = kwargs[ATTR_TRANSITION] * 10 + _LOGGER.debug("turn_on requested transition time for light:" + " %s is: %s ", + self._name, transition) else: - fade = 0 + transition = 0 + _LOGGER.debug("turn_on requested transition time for light:" + " %s is: %s ", + self._name, transition) if ATTR_RGB_COLOR in kwargs: red, green, blue = kwargs[ATTR_RGB_COLOR] - self._light.set_rgb(red, green, blue, fade) - - if ATTR_BRIGHTNESS in kwargs: - brightness = int(kwargs[ATTR_BRIGHTNESS] / 2.55) + _LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:" + " %s is: %s %s %s ", + self._name, red, green, blue) + self._light.set_rgb(red, green, blue, transition) if ATTR_COLOR_TEMP in kwargs: color_t = kwargs[ATTR_COLOR_TEMP] kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) / (TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN) - self._light.set_temperature(kelvin, fade) + _LOGGER.debug("turn_on requested set_temperature for light:" + " %s: %s ", self._name, kelvin) + self._light.set_temperature(kelvin, transition) - effect = kwargs.get(ATTR_EFFECT) - if effect == EFFECT_RANDOM: - self._light.set_rgb(random.randrange(0, 255), - random.randrange(0, 255), - random.randrange(0, 255), - fade) + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", + self._name, self._brightness) + self._brightness = self._light.set_luminance( + int(self._brightness / 2.55), + transition) + + if ATTR_EFFECT in kwargs: + effect = kwargs.get(ATTR_EFFECT) + if effect == EFFECT_RANDOM: + self._light.set_rgb(random.randrange(0, 255), + random.randrange(0, 255), + random.randrange(0, 255), + transition) + _LOGGER.debug("turn_on requested random effect for light:" + " %s with transition %s ", + self._name, transition) - self._light.set_luminance(brightness, fade) self.update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" + _LOGGER.debug("turn_off Attempting to turn off light: %s ", + self._name) if ATTR_TRANSITION in kwargs: - fade = kwargs[ATTR_TRANSITION] * 10 + transition = kwargs[ATTR_TRANSITION] * 10 + _LOGGER.debug("turn_off requested transition time for light:" + " %s is: %s ", + self._name, transition) + self._light.set_luminance(0, transition) else: - fade = 0 - self._light.set_luminance(0, fade) + transition = 0 + _LOGGER.debug("turn_off requested transition time for light:" + " %s is: %s ", + self._name, transition) + self._light.set_onoff(0) + self._state = self._light.on() + self.update_ha_state() def update(self): """Synchronize state with bridge.""" self.update_lights(no_throttle=True) + self._brightness = int(self._light.lum() * 2.55) + self._name = self._light.name() + self._rgb = self._light.rgb() + o_temp = self._light.temp() + self._temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) + * (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN)) + self._state = self._light.on() diff --git a/requirements_all.txt b/requirements_all.txt index 80572e80016..d948e1958c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -226,6 +226,9 @@ https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e22462 # homeassistant.components.scene.hunterdouglas_powerview https://github.com/sander76/powerviewApi/archive/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15 +# homeassistant.components.light.osramlightify +https://github.com/tfriedel/python-lightify/archive/d6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4 + # homeassistant.components.mysensors https://github.com/theolind/pymysensors/archive/0b705119389be58332f17753c53167f551254b6c.zip#pymysensors==0.8 @@ -258,9 +261,6 @@ libnacl==1.5.0 # homeassistant.components.light.lifx liffylights==0.9.4 -# homeassistant.components.light.osramlightify -lightify==1.0.3 - # homeassistant.components.light.limitlessled limitlessled==1.0.2 From 0c6ef3b7f922116351e99c773e085276ba171a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 23 Nov 2016 07:16:01 +0100 Subject: [PATCH 033/137] Try to register a Chromecast anyway, even if it could not be detected by get_chromecasts(), since it might be on a other network. Fixes #4469. (#4470) --- homeassistant/components/media_player/cast.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 1ec61cc621a..1d01f0058ec 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -81,6 +81,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): KNOWN_HOSTS.append(host) except pychromecast.ChromecastConnectionError: pass + else: + try: + # add the device anyway, get_chromecasts couldn't find it + casts.append(CastDevice(pychromecast.Chromecast(*host))) + KNOWN_HOSTS.append(host) + except pychromecast.ChromecastConnectionError: + pass add_devices(casts) From 260a619a40d5a8e789d04513a44fab93b44848c9 Mon Sep 17 00:00:00 2001 From: dainok Date: Wed, 23 Nov 2016 07:19:57 +0100 Subject: [PATCH 034/137] Added GPSLogger API (#4089) * Added GPSLogger API, check https://goo.gl/eJnKw5 for details. * Switched to debug severity and added to coveragerc * Switched to debug severity for logs * Updated .coveragerc * Update .coveragerc * Merged from sfiorini * Merged from sfiorini * Update .coveragerc --- .coveragerc | 1 + .../components/device_tracker/gpslogger.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 homeassistant/components/device_tracker/gpslogger.py diff --git a/.coveragerc b/.coveragerc index d572fe7afc0..17a6fc4317c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -152,6 +152,7 @@ omit = homeassistant/components/device_tracker/bt_home_hub_5.py homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/fritz.py + homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py new file mode 100644 index 00000000000..462ae16300c --- /dev/null +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -0,0 +1,74 @@ +""" +Support for the GPSLogger platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.gpslogger/ +""" +import asyncio +from functools import partial +import logging + +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY +from homeassistant.components.http import HomeAssistantView +# pylint: disable=unused-import +from homeassistant.components.device_tracker import ( # NOQA + DOMAIN, PLATFORM_SCHEMA) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['http'] + + +def setup_scanner(hass, config, see): + """Setup an endpoint for the GPSLogger application.""" + hass.http.register_view(GPSLoggerView(hass, see)) + + return True + + +class GPSLoggerView(HomeAssistantView): + """View to handle gpslogger requests.""" + + url = '/api/gpslogger' + name = 'api:gpslogger' + + def __init__(self, hass, see): + """Initialize GPSLogger url endpoints.""" + super().__init__(hass) + self.see = see + + @asyncio.coroutine + def get(self, request): + """A GPSLogger message received as GET.""" + res = yield from self._handle(request.GET) + return res + + @asyncio.coroutine + # pylint: disable=too-many-return-statements + def _handle(self, data): + """Handle gpslogger request.""" + if 'latitude' not in data or 'longitude' not in data: + return ('Latitude and longitude not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'device' not in data: + _LOGGER.error('Device id not specified.') + return ('Device id not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + device = data['device'].replace('-', '') + gps_location = (data['latitude'], data['longitude']) + accuracy = 200 + battery = -1 + + if 'accuracy' in data: + accuracy = int(float(data['accuracy'])) + if 'battery' in data: + battery = float(data['battery']) + + yield from self.hass.loop.run_in_executor( + None, partial(self.see, dev_id=device, + gps=gps_location, battery=battery, + gps_accuracy=accuracy)) + + return 'Setting location for {}'.format(device) From 85d6970df88f8f665ceee566dd59aaf9e69a0f21 Mon Sep 17 00:00:00 2001 From: Harris Borawski Date: Tue, 22 Nov 2016 22:32:45 -0800 Subject: [PATCH 035/137] Add Sensor for Sonarr (#4496) * Add sonarr sensor and tests for sensor * Fixed some linting errors and removed unused import * Add SSL option for those who use SSL from within Sonarr * Add requirements to all requirements, and sensor to coveragerc * remove unused variable * move methods to functions, and other lint fixes * linting fixes * linting is clean now * Remove double requirement * fix linting for docstrings, this should probably be a part of the script/lint and not just travis --- .coveragerc | 1 + homeassistant/components/sensor/sonarr.py | 237 +++++++ tests/components/sensor/test_sonarr.py | 811 ++++++++++++++++++++++ 3 files changed, 1049 insertions(+) create mode 100644 homeassistant/components/sensor/sonarr.py create mode 100644 tests/components/sensor/test_sonarr.py diff --git a/.coveragerc b/.coveragerc index 17a6fc4317c..19c02da166d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -292,6 +292,7 @@ omit = homeassistant/components/sensor/scrape.py homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/snmp.py + homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/supervisord.py diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py new file mode 100644 index 00000000000..0023755bc04 --- /dev/null +++ b/homeassistant/components/sensor/sonarr.py @@ -0,0 +1,237 @@ +"""Support for Sonarr.""" +import logging +import time +from datetime import datetime +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.const import CONF_SSL +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +_LOGGER = logging.getLogger(__name__) + +CONF_HOST = 'host' +CONF_PORT = 'port' +CONF_DAYS = 'days' +CONF_INCLUDED = 'include_paths' +CONF_UNIT = 'unit' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8989 +DEFAULT_DAYS = '1' +DEFAULT_UNIT = 'GB' + +SENSOR_TYPES = { + 'diskspace': ['Disk Space', 'GB', 'mdi:harddisk'], + 'queue': ['Queue', 'Episodes', 'mdi:download'], + 'upcoming': ['Upcoming', 'Episodes', 'mdi:television'], + 'wanted': ['Wanted', 'Episodes', 'mdi:television'], + 'series': ['Series', 'Shows', 'mdi:television'], + 'commands': ['Commands', 'Commands', 'mdi:code-braces'] +} + +ENDPOINTS = { + 'diskspace': 'http{0}://{1}:{2}/api/diskspace?apikey={3}', + 'queue': 'http{0}://{1}:{2}/api/queue?apikey={3}', + 'upcoming': 'http{0}://{1}:{2}/api/calendar?apikey={3}&start={4}&end={5}', + 'wanted': 'http{0}://{1}:{2}/api/wanted/missing?apikey={3}', + 'series': 'http{0}://{1}:{2}/api/series?apikey={3}', + 'commands': 'http{0}://{1}:{2}/api/command?apikey={3}' +} + +# Suport to Yottabytes for the future, why not +BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]), + vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.string, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): vol.In(BYTE_SIZES) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Sonarr platform.""" + conditions = config.get(CONF_MONITORED_CONDITIONS) + add_devices( + [Sonarr(hass, config, sensor) for sensor in conditions] + ) + return True + + +class Sonarr(Entity): + """Implement the Sonarr sensor class.""" + + def __init__(self, hass, conf, sensor_type): + """Create sonarr entity.""" + from pytz import timezone + # Configuration data + self.conf = conf + self.host = conf.get(CONF_HOST) + self.port = conf.get(CONF_PORT) + self.apikey = conf.get(CONF_API_KEY) + self.included = conf.get(CONF_INCLUDED) + self.days = int(conf.get(CONF_DAYS)) + self.ssl = 's' if conf.get(CONF_SSL) else '' + + # Object data + self._tz = timezone(str(hass.config.time_zone)) + self.type = sensor_type + self._name = SENSOR_TYPES[self.type][0] + if self.type == 'diskspace': + self._unit = conf.get(CONF_UNIT) + else: + self._unit = SENSOR_TYPES[self.type][1] + self._icon = SENSOR_TYPES[self.type][2] + + # Update sensor + self.update() + + def update(self): + """Update the data for the sensor.""" + start = get_date(self._tz) + end = get_date(self._tz, self.days) + res = requests.get( + ENDPOINTS[self.type].format( + self.ssl, + self.host, + self.port, + self.apikey, + start, + end + ) + ) + if res.status_code == 200: + if self.type in ['upcoming', 'queue', 'series', 'commands']: + if self.days == 1 and self.type == 'upcoming': + # Sonarr API returns empty array if start and end dates are + # the same, so we need to filter to just today + self.data = list( + filter( + lambda x: x['airDate'] == str(start), + res.json() + ) + ) + else: + self.data = res.json() + self._state = len(self.data) + elif self.type == 'wanted': + data = res.json() + res = requests.get('{}&pageSize={}'.format( + ENDPOINTS[self.type].format( + self.ssl, + self.host, + self.port, + self.apikey + ), + data['totalRecords'] + )) + self.data = res.json()['records'] + self._state = len(self.data) + elif self.type == 'diskspace': + # If included paths are not provided, use all data + if self.included == []: + self.data = res.json() + else: + # Filter to only show lists that are included + self.data = list( + filter( + lambda x: x['path'] in self.included, + res.json() + ) + ) + self._state = '{:.2f}'.format( + to_unit( + sum([data['freeSpace'] for data in self.data]), + self._unit + ) + ) + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format("Sonarr", self._name) + + @property + def state(self): + """Return sensor state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + attributes = {} + if self.type == 'upcoming': + for show in self.data: + attributes[show['series']['title']] = 'S{:02d}E{:02d}'.format( + show['seasonNumber'], + show['episodeNumber'] + ) + elif self.type == 'queue': + for show in self.data: + attributes[show['series']['title'] + ' S{:02d}E{:02d}'.format( + show['episode']['seasonNumber'], + show['episode']['episodeNumber'] + )] = '{:.2f}%'.format(100*(1-(show['sizeleft']/show['size']))) + elif self.type == 'wanted': + for show in self.data: + attributes[show['series']['title'] + ' S{:02d}E{:02d}'.format( + show['seasonNumber'], + show['episodeNumber'] + )] = show['airDate'] + elif self.type == 'commands': + for command in self.data: + attributes[command['name']] = command['state'] + elif self.type == 'diskspace': + for data in self.data: + attributes[data['path']] = '{:.2f}/{:.2f}{} ({:.2f}%)'.format( + to_unit(data['freeSpace'], self._unit), + to_unit(data['totalSpace'], self._unit), + self._unit, + ( + to_unit( + data['freeSpace'], + self._unit + ) / + to_unit( + data['totalSpace'], + self._unit + )*100 + ) + ) + elif self.type == 'series': + for show in self.data: + attributes[show['title']] = '{}/{} Episodes'.format( + show['episodeFileCount'], + show['episodeCount'] + ) + return attributes + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + + +def get_date(zone, offset=0): + """Get date based on timezone and offset of days.""" + day = 60*60*24 + return datetime.date( + datetime.fromtimestamp(time.time() + day*offset, tz=zone) + ) + + +def to_unit(value, unit): + """Convert bytes to give unit.""" + return value/1024**BYTE_SIZES.index(unit) diff --git a/tests/components/sensor/test_sonarr.py b/tests/components/sensor/test_sonarr.py new file mode 100644 index 00000000000..e483a3aee1e --- /dev/null +++ b/tests/components/sensor/test_sonarr.py @@ -0,0 +1,811 @@ +"""The tests for the sonarr platform.""" +import unittest +import time +from datetime import datetime +from homeassistant.components.sensor import sonarr + +from tests.common import get_test_home_assistant + + +def mocked_requests_get(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + today = datetime.date(datetime.fromtimestamp(time.time())) + url = str(args[0]) + if 'api/calendar' in url: + return MockResponse([ + { + "seriesId": 3, + "episodeFileId": 0, + "seasonNumber": 4, + "episodeNumber": 11, + "title": "Easy Com-mercial, Easy Go-mercial", + "airDate": str(today), + "airDateUtc": "2014-01-27T01:30:00Z", + "overview": "To compete with fellow ā€œrestaurateur,ā€ Ji...", + "hasFile": "false", + "monitored": "true", + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "tvdbId": 194031, + "tvRageId": 24607, + "imdbId": "tt1561755", + "title": "Bob's Burgers", + "cleanTitle": "bobsburgers", + "status": "continuing", + "overview": "Bob's Burgers follows a third-generation ...", + "airTime": "5:30pm", + "monitored": "true", + "qualityProfileId": 1, + "seasonFolder": "true", + "lastInfoSync": "2014-01-26T19:25:55.4555946Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us/images/bann.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.us/images/poster00.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/images/fan6.jpg" + } + ], + "seriesType": "standard", + "network": "FOX", + "useSceneNumbering": "false", + "titleSlug": "bobs-burgers", + "path": "T:\\Bob's Burgers", + "year": 0, + "firstAired": "2011-01-10T01:30:00Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": "true" + }, + "seasons": [ + { + "seasonNumber": 4, + "monitored": "true" + }, + { + "seasonNumber": 3, + "monitored": "true" + }, + { + "seasonNumber": 2, + "monitored": "true" + }, + { + "seasonNumber": 1, + "monitored": "true" + }, + { + "seasonNumber": 0, + "monitored": "false" + } + ], + "id": 66 + }, + "downloading": "false", + "id": 14402 + } + ], 200) + elif 'api/command' in url: + return MockResponse([ + { + "name": "RescanSeries", + "startedOn": "0001-01-01T00:00:00Z", + "stateChangeTime": "2014-02-05T05:09:09.2366139Z", + "sendUpdatesToClient": "true", + "state": "pending", + "id": 24 + } + ], 200) + elif 'api/wanted/missing' in url or 'totalRecords' in url: + return MockResponse( + { + "page": 1, + "pageSize": 15, + "sortKey": "airDateUtc", + "sortDirection": "descending", + "totalRecords": 1, + "records": [ + { + "seriesId": 1, + "episodeFileId": 0, + "seasonNumber": 5, + "episodeNumber": 4, + "title": "Archer Vice: House Call", + "airDate": "2014-02-03", + "airDateUtc": "2014-02-04T03:00:00Z", + "overview": "Archer has to stage an that ... ", + "hasFile": "false", + "monitored": "true", + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "absoluteEpisodeNumber": 50, + "series": { + "tvdbId": 110381, + "tvRageId": 23354, + "imdbId": "tt1486217", + "title": "Archer (2009)", + "cleanTitle": "archer2009", + "status": "continuing", + "overview": "At ISIS, an international spy ...", + "airTime": "7:00pm", + "monitored": "true", + "qualityProfileId": 1, + "seasonFolder": "true", + "lastInfoSync": "2014-02-05T04:39:28.550495Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us//57.12.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.u/57.12-300.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/image.12.jpg" + } + ], + "seriesType": "standard", + "network": "FX", + "useSceneNumbering": "false", + "titleSlug": "archer-2009", + "path": "E:\\Test\\TV\\Archer (2009)", + "year": 2009, + "firstAired": "2009-09-18T02:00:00Z", + "qualityProfile": { + "value": { + "name": "SD", + "cutoff": { + "id": 1, + "name": "SDTV" + }, + "items": [ + { + "quality": { + "id": 1, + "name": "SDTV" + }, + "allowed": "true" + }, + { + "quality": { + "id": 8, + "name": "WEBDL-480p" + }, + "allowed": "true" + }, + { + "quality": { + "id": 2, + "name": "DVD" + }, + "allowed": "true" + }, + { + "quality": { + "id": 4, + "name": "HDTV-720p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 9, + "name": "HDTV-1080p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 10, + "name": "Raw-HD" + }, + "allowed": "false" + }, + { + "quality": { + "id": 5, + "name": "WEBDL-720p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 6, + "name": "Bluray-720p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 3, + "name": "WEBDL-1080p" + }, + "allowed": "false" + }, + { + "quality": { + "id": 7, + "name": "Bluray-1080p" + }, + "allowed": "false" + } + ], + "id": 1 + }, + "isLoaded": "true" + }, + "seasons": [ + { + "seasonNumber": 5, + "monitored": "true" + }, + { + "seasonNumber": 4, + "monitored": "true" + }, + { + "seasonNumber": 3, + "monitored": "true" + }, + { + "seasonNumber": 2, + "monitored": "true" + }, + { + "seasonNumber": 1, + "monitored": "true" + }, + { + "seasonNumber": 0, + "monitored": "false" + } + ], + "id": 1 + }, + "downloading": "false", + "id": 55 + } + ] + }, 200) + elif 'api/queue' in url: + return MockResponse([ + { + "series": { + "title": "Game of Thrones", + "sortTitle": "game thrones", + "seasonCount": 6, + "status": "continuing", + "overview": "Seven noble families fight for land ...", + "network": "HBO", + "airTime": "21:00", + "images": [ + { + "coverType": "fanart", + "url": "http://thetvdb.com/banners/fanart/-83.jpg" + }, + { + "coverType": "banner", + "url": "http://thetvdb.com/banners/-g19.jpg" + }, + { + "coverType": "poster", + "url": "http://thetvdb.com/banners/posters-34.jpg" + } + ], + "seasons": [ + { + "seasonNumber": 0, + "monitored": "false" + }, + { + "seasonNumber": 1, + "monitored": "false" + }, + { + "seasonNumber": 2, + "monitored": "true" + }, + { + "seasonNumber": 3, + "monitored": "false" + }, + { + "seasonNumber": 4, + "monitored": "false" + }, + { + "seasonNumber": 5, + "monitored": "true" + }, + { + "seasonNumber": 6, + "monitored": "true" + } + ], + "year": 2011, + "path": "/Volumes/Media/Shows/Game of Thrones", + "profileId": 5, + "seasonFolder": "true", + "monitored": "true", + "useSceneNumbering": "false", + "runtime": 60, + "tvdbId": 121361, + "tvRageId": 24493, + "tvMazeId": 82, + "firstAired": "2011-04-16T23:00:00Z", + "lastInfoSync": "2016-02-05T16:40:11.614176Z", + "seriesType": "standard", + "cleanTitle": "gamethrones", + "imdbId": "tt0944947", + "titleSlug": "game-of-thrones", + "certification": "TV-MA", + "genres": [ + "Adventure", + "Drama", + "Fantasy" + ], + "tags": [], + "added": "2015-12-28T13:44:24.204583Z", + "ratings": { + "votes": 1128, + "value": 9.4 + }, + "qualityProfileId": 5, + "id": 17 + }, + "episode": { + "seriesId": 17, + "episodeFileId": 0, + "seasonNumber": 3, + "episodeNumber": 8, + "title": "Second Sons", + "airDate": "2013-05-19", + "airDateUtc": "2013-05-20T01:00:00Z", + "overview": "King’s Landing hosts a wedding, and ...", + "hasFile": "false", + "monitored": "false", + "absoluteEpisodeNumber": 28, + "unverifiedSceneNumbering": "false", + "id": 889 + }, + "quality": { + "quality": { + "id": 7, + "name": "Bluray-1080p" + }, + "revision": { + "version": 1, + "real": 0 + } + }, + "size": 4472186820, + "title": "Game.of.Thrones.S03E08.Second.Sons.2013.1080p.", + "sizeleft": 0, + "timeleft": "00:00:00", + "estimatedCompletionTime": "2016-02-05T22:46:52.440104Z", + "status": "Downloading", + "trackedDownloadStatus": "Ok", + "statusMessages": [], + "downloadId": "SABnzbd_nzo_Mq2f_b", + "protocol": "usenet", + "id": 1503378561 + } + ], 200) + elif 'api/series' in url: + return MockResponse([ + { + "title": "Marvel's Daredevil", + "alternateTitles": [{ + "title": "Daredevil", + "seasonNumber": -1 + }], + "sortTitle": "marvels daredevil", + "seasonCount": 2, + "totalEpisodeCount": 26, + "episodeCount": 26, + "episodeFileCount": 26, + "sizeOnDisk": 79282273693, + "status": "continuing", + "overview": "Matt Murdock was blinded in a tragic accident...", + "previousAiring": "2016-03-18T04:01:00Z", + "network": "Netflix", + "airTime": "00:01", + "images": [ + { + "coverType": "fanart", + "url": "/sonarr/MediaCover/7/fanart.jpg?lastWrite=" + }, + { + "coverType": "banner", + "url": "/sonarr/MediaCover/7/banner.jpg?lastWrite=" + }, + { + "coverType": "poster", + "url": "/sonarr/MediaCover/7/poster.jpg?lastWrite=" + } + ], + "seasons": [ + { + "seasonNumber": 1, + "monitored": "false", + "statistics": { + "previousAiring": "2015-04-10T04:01:00Z", + "episodeFileCount": 13, + "episodeCount": 13, + "totalEpisodeCount": 13, + "sizeOnDisk": 22738179333, + "percentOfEpisodes": 100 + } + }, + { + "seasonNumber": 2, + "monitored": "false", + "statistics": { + "previousAiring": "2016-03-18T04:01:00Z", + "episodeFileCount": 13, + "episodeCount": 13, + "totalEpisodeCount": 13, + "sizeOnDisk": 56544094360, + "percentOfEpisodes": 100 + } + } + ], + "year": 2015, + "path": "F:\\TV_Shows\\Marvels Daredevil", + "profileId": 6, + "seasonFolder": "true", + "monitored": "true", + "useSceneNumbering": "false", + "runtime": 55, + "tvdbId": 281662, + "tvRageId": 38796, + "tvMazeId": 1369, + "firstAired": "2015-04-10T04:00:00Z", + "lastInfoSync": "2016-09-09T09:02:49.4402575Z", + "seriesType": "standard", + "cleanTitle": "marvelsdaredevil", + "imdbId": "tt3322312", + "titleSlug": "marvels-daredevil", + "certification": "TV-MA", + "genres": [ + "Action", + "Crime", + "Drama" + ], + "tags": [], + "added": "2015-05-15T00:20:32.7892744Z", + "ratings": { + "votes": 461, + "value": 8.9 + }, + "qualityProfileId": 6, + "id": 7 + } + ], 200) + elif 'api/diskspace' in url: + return MockResponse([ + { + "path": "/data", + "label": "", + "freeSpace": 282500067328, + "totalSpace": 499738734592 + } + ], 200) + else: + return MockResponse({ + "error": "Unauthorized" + }, 401) + + +class TestSonarrSetup(unittest.TestCase): + """Test the Sonarr platform.""" + + # pylint: disable=invalid-name + DEVICES = [] + + def add_devices(self, devices): + """Mock add devices.""" + for device in devices: + self.DEVICES.append(device) + + def setUp(self): + """Initialize values for this testcase class.""" + self.DEVICES = [] + self.hass = get_test_home_assistant() + self.hass.config.time_zone = 'America/Los_Angeles' + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_diskspace_no_paths(self, req_mock): + """Tests getting all disk space""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [], + 'monitored_conditions': [ + 'diskspace' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual('263.10', device.state) + self.assertEqual('mdi:harddisk', device.icon) + self.assertEqual('GB', device.unit_of_measurement) + self.assertEqual('Sonarr Disk Space', device.name) + self.assertEqual( + '263.10/465.42GB (56.53%)', + device.device_state_attributes["/data"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_diskspace_paths(self, req_mock): + """Tests getting diskspace for included paths""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'diskspace' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual('263.10', device.state) + self.assertEqual('mdi:harddisk', device.icon) + self.assertEqual('GB', device.unit_of_measurement) + self.assertEqual('Sonarr Disk Space', device.name) + self.assertEqual( + '263.10/465.42GB (56.53%)', + device.device_state_attributes["/data"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_commands(self, req_mock): + """Tests getting running commands""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'commands' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:code-braces', device.icon) + self.assertEqual('Commands', device.unit_of_measurement) + self.assertEqual('Sonarr Commands', device.name) + self.assertEqual( + 'pending', + device.device_state_attributes["RescanSeries"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_queue(self, req_mock): + """Tests getting downloads in the queue""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'queue' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:download', device.icon) + self.assertEqual('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Queue', device.name) + self.assertEqual( + '100.00%', + device.device_state_attributes["Game of Thrones S03E08"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_series(self, req_mock): + """Tests getting the number of series""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'series' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Shows', device.unit_of_measurement) + self.assertEqual('Sonarr Series', device.name) + self.assertEqual( + '26/26 Episodes', + device.device_state_attributes["Marvel's Daredevil"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_wanted(self, req_mock): + """Tests getting wanted episodes""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'wanted' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Wanted', device.name) + self.assertEqual( + '2014-02-03', + device.device_state_attributes["Archer (2009) S05E04"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_upcoming_multiple_days(self, req_mock): + """Tests upcoming episodes for multiple days""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '2', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Upcoming', device.name) + self.assertEqual( + 'S04E11', + device.device_state_attributes["Bob's Burgers"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_upcoming_today(self, req_mock): + """ + Tests filtering for a single day. + Sonarr needs to respond with at least 2 days + """ + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Upcoming', device.name) + self.assertEqual( + 'S04E11', + device.device_state_attributes["Bob's Burgers"] + ) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_ssl(self, req_mock): + """Tests SSL being enabled""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ], + "ssl": "true" + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(1, device.state) + self.assertEqual('s', device.ssl) + self.assertEqual('mdi:television', device.icon) + self.assertEqual('Episodes', device.unit_of_measurement) + self.assertEqual('Sonarr Upcoming', device.name) + self.assertEqual( + 'S04E11', + device.device_state_attributes["Bob's Burgers"] + ) From c294a534d0b6176cc35502f1f05f550a4187c1e6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 23 Nov 2016 07:47:43 +0100 Subject: [PATCH 036/137] Migrate binary_sensor to async (#4516) --- homeassistant/components/binary_sensor/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 18e33ffe738..38b08fd32b4 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -4,6 +4,7 @@ Component to interface with binary sensors. For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor/ """ +import asyncio import logging import voluptuous as vol @@ -39,13 +40,13 @@ SENSOR_CLASSES = [ SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES)) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track states and offer events for binary sensors.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - component.setup(config) - + yield from component.async_setup(config) return True From 3f9250415f574b4db0250728cb40316c661e1370 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Nov 2016 22:58:14 -0800 Subject: [PATCH 037/137] Skip broken tests (#4543) --- tests/components/sensor/test_sonarr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/components/sensor/test_sonarr.py b/tests/components/sensor/test_sonarr.py index e483a3aee1e..c44d01f3ce0 100644 --- a/tests/components/sensor/test_sonarr.py +++ b/tests/components/sensor/test_sonarr.py @@ -2,6 +2,9 @@ import unittest import time from datetime import datetime + +import pytest + from homeassistant.components.sensor import sonarr from tests.common import get_test_home_assistant @@ -751,6 +754,7 @@ class TestSonarrSetup(unittest.TestCase): device.device_state_attributes["Bob's Burgers"] ) + @pytest.mark.skip @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) def test_upcoming_today(self, req_mock): """ @@ -781,6 +785,7 @@ class TestSonarrSetup(unittest.TestCase): device.device_state_attributes["Bob's Burgers"] ) + @pytest.mark.skip @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) def test_ssl(self, req_mock): """Tests SSL being enabled""" From bb46009efa0868cc1d610e55d024e4b57e0dbcac Mon Sep 17 00:00:00 2001 From: Valentin Alexeev Date: Wed, 23 Nov 2016 08:59:27 +0200 Subject: [PATCH 038/137] World Air Quality Index sensor (#4434) * Implement WAQI sensor * Corrections based on CI check. * Updated requirements_all.txt for pwaqi==1.2 * Require latest version of pwaqi * Fix lint: single argument for .exception and no more pass statement. * Further lint fixes. * pydocstyle fix * Implement rate throttle. Data on WAQI is usually updated once an hour - make it refresh every thirty minutes. * Implement schema validation with voluptuous. Change exception handling scope. Move messages to debug(). * Fix lint (empty indented line). * Sort lines correctly. * Fix last lint issue. * Provide additional sensor data as received from WAQI. Easier-to-read throttle timing. * Additional object attributes to be unrolled later. --- .coveragerc | 1 + homeassistant/components/sensor/waqi.py | 97 +++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 101 insertions(+) create mode 100644 homeassistant/components/sensor/waqi.py diff --git a/.coveragerc b/.coveragerc index 19c02da166d..0840e5bcb4d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -308,6 +308,7 @@ omit = homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py homeassistant/components/sensor/vasttrafik.py + homeassistant/components/sensor/waqi.py homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py homeassistant/components/switch/acer_projector.py diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py new file mode 100644 index 00000000000..3acb507f86d --- /dev/null +++ b/homeassistant/components/sensor/waqi.py @@ -0,0 +1,97 @@ +""" +Support for the World Air Quality Index service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.waqi/ +""" +import logging +from datetime import timedelta +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv +import voluptuous as vol + +REQUIREMENTS = ["pwaqi==1.2"] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'aqi': ['AQI', '0-300+', 'mdi:cloud'] +} + +ATTR_LOCATION = 'locations' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(ATTR_LOCATION): cv.ensure_list +}) + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the requested World Air Quality Index locations.""" + dev = [] + import pwaqi + # Iterate each module + for location_name in config[ATTR_LOCATION]: + _LOGGER.debug('Adding location %s', location_name) + station_ids = pwaqi.findStationCodesByCity(location_name) + _LOGGER.debug('I got the following stations: %s', station_ids) + for station in station_ids: + dev.append(WaqiSensor(station)) + + add_devices(dev) + + +# pylint: disable=too-few-public-methods +class WaqiSensor(Entity): + """Implementation of a WAQI sensor.""" + + def __init__(self, station_id): + """Initialize the sensor.""" + self._station_id = station_id + self._state = None + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + if 'city' in self._data: + return "WAQI {}".format(self._data['city']['name']) + return "WAQI {}".format(self._station_id) + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return "mdi:cloud" + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return "AQI" + + @property + def state_attributes(self): + """Return the state attributes of the last update.""" + return { + "time": self._data.get('time', 'no data'), + "dominentpol": self._data.get('dominentpol', 'no data') + } + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the data from World Air Quality Index and updates the states.""" + import pwaqi + try: + self._data = pwaqi.getStationObservation(self._station_id) + + self._state = self._data.get('aqi', 'no data') + except KeyError: + _LOGGER.exception('Unable to fetch data from WAQI.') diff --git a/requirements_all.txt b/requirements_all.txt index d948e1958c2..c5257dd0dde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -330,6 +330,9 @@ pushbullet.py==0.10.0 # homeassistant.components.notify.pushetta pushetta==1.0.15 +# homeassistant.components.sensor.waqi +pwaqi==1.2 + # homeassistant.components.sensor.cpuspeed py-cpuinfo==0.2.3 From 64cfc4ff0283b2cca515377045a3c0ab9047fb6e Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Wed, 23 Nov 2016 08:03:39 +0100 Subject: [PATCH 039/137] DSMR sensor (#4309) * Initial implemenation of DSMR component. * Fix linting * Remove protocol V2.2 support until merged upstream. * Generate requirements using script. * Use updated dsmr-parser with protocol 2.2 support. * Add tests. * Isort and input validation. * Add entities for gas and actual meter reading. Error handling. Use Throttle. * Implement non-blocking serial reader. * Improve logging. * Merge entities into one, add icons, fix tests for asyncio. * Add error logging for serial reader. * Refactoring and documentation. - refactor asyncio reader task to make sure it stops with HA - document general principle of this component - refactor entity reading to be more clear - remove cruft from split entity implementation * Use `port` configuration key. * DSMR V2.2 seems to conflict in explaining which tariff is high and low. http://www.netbeheernederland.nl/themas/hotspot/hotspot-documenten/?dossierid=11010056&title=Slimme%20meter&onderdeel=Documenten > DSMR v2.2 Final P1 >> 6.1: table vs table note Meter Reading electricity delivered to client normal tariff) in 0,01 kWh - 1-0:1.8.1.255 Meter Reading electricity delivered to client (low tariff) in 0,01 kWh - 1-0:1.8.2.255 Note: Tariff code 1 is used for low tariff and tariff code 2 is used for normal tariff. * Refactor to use asyncio.Protocol instead of loop+queue. * Fix requirements * Close transport when HA stops. * Cleanup. * Include as dependency for testing (until merged upstream.) * Fix style. * Update setup.cfg --- homeassistant/components/sensor/dsmr.py | 179 ++++++++++++++++++++++++ requirements_all.txt | 3 + tests/components/sensor/test_dsmr.py | 64 +++++++++ 3 files changed, 246 insertions(+) create mode 100644 homeassistant/components/sensor/dsmr.py create mode 100644 tests/components/sensor/test_dsmr.py diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py new file mode 100644 index 00000000000..eb8e5174b47 --- /dev/null +++ b/homeassistant/components/sensor/dsmr.py @@ -0,0 +1,179 @@ +""" +Support for Dutch Smart Meter Requirements. + +Also known as: Smartmeter or P1 port. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.dsmr/ + +Technical overview: + +DSMR is a standard to which Dutch smartmeters must comply. It specifies that +the smartmeter must send out a 'telegram' every 10 seconds over a serial port. + +The contents of this telegram differ between version but they generally consist +of lines with 'obis' (Object Identification System, a numerical ID for a value) +followed with the value and unit. + +This module sets up a asynchronous reading loop using the `dsmr_parser` module +which waits for a complete telegram, parser it and puts it on an async queue as +a dictionary of `obis`/object mapping. The numeric value and unit of each value +can be read from the objects attributes. Because the `obis` are know for each +DSMR version the Entities for this component are create during bootstrap. + +Another loop (DSMR class) is setup which reads the telegram queue, +stores/caches the latest telegram and notifies the Entities that the telegram +has been updated. +""" + +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity + +DOMAIN = 'dsmr' + +REQUIREMENTS = [ + 'https://github.com/aequitas/dsmr_parser/archive/async_protocol.zip' + '#dsmr_parser==0.4' +] + +# Smart meter sends telegram every 10 seconds +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + +CONF_DSMR_VERSION = 'dsmr_version' +DEFAULT_PORT = '/dev/ttyUSB0' +DEFAULT_DSMR_VERSION = '2.2' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( + cv.string, vol.In(['4', '2.2'])), +}) + +_LOGGER = logging.getLogger(__name__) + +ICON_POWER = 'mdi:flash' +ICON_GAS = 'mdi:fire' + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup DSMR sensors.""" + # suppres logging + logging.getLogger('dsmr_parser').setLevel(logging.ERROR) + + from dsmr_parser import obis_references as obis + from dsmr_parser.protocol import create_dsmr_reader + + dsmr_version = config[CONF_DSMR_VERSION] + + # define list of name,obis mappings to generate entities + obis_mapping = [ + ['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE], + ['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY], + ['Power Tariff', obis.ELECTRICITY_ACTIVE_TARIFF], + ['Power Consumption (low)', obis.ELECTRICITY_USED_TARIFF_1], + ['Power Consumption (normal)', obis.ELECTRICITY_USED_TARIFF_2], + ['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1], + ['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2], + ] + # protocol version specific obis + if dsmr_version == '4': + obis_mapping.append(['Gas Consumption', obis.HOURLY_GAS_METER_READING]) + else: + obis_mapping.append(['Gas Consumption', obis.GAS_METER_READING]) + + # generate device entities + devices = [DSMREntity(name, obis) for name, obis in obis_mapping] + + # setup devices + yield from async_add_devices(devices) + + def update_entities_telegram(telegram): + """Update entities with latests telegram & trigger state update.""" + # make all device entities aware of new telegram + for device in devices: + device.telegram = telegram + hass.async_add_job(device.async_update_ha_state) + + # creates a asyncio.Protocol for reading DSMR telegrams from serial + # and calls update_entities_telegram to update entities on arrival + dsmr = create_dsmr_reader(config[CONF_PORT], config[CONF_DSMR_VERSION], + update_entities_telegram, loop=hass.loop) + + # start DSMR asycnio.Protocol reader + transport, _ = yield from hass.loop.create_task(dsmr) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, transport.close) + + +class DSMREntity(Entity): + """Entity reading values from DSMR telegram.""" + + def __init__(self, name, obis): + """"Initialize entity.""" + # human readable name + self._name = name + # DSMR spec. value identifier + self._obis = obis + self.telegram = {} + + def get_dsmr_object_attr(self, attribute): + """Read attribute from last received telegram for this DSMR object.""" + # make sure telegram contains an object for this entities obis + if self._obis not in self.telegram: + return None + + # get the attibute value if the object has it + dsmr_object = self.telegram[self._obis] + return getattr(dsmr_object, attribute, None) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if 'Power' in self._name: + return ICON_POWER + elif 'Gas' in self._name: + return ICON_GAS + + @property + def state(self): + """Return the state of sensor, if available, translate if needed.""" + from dsmr_parser import obis_references as obis + + value = self.get_dsmr_object_attr('value') + + if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: + return self.translate_tariff(value) + else: + return value + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self.get_dsmr_object_attr('unit') + + @staticmethod + def translate_tariff(value): + """Convert 2/1 to normal/low.""" + # DSMR V2.2: Note: Tariff code 1 is used for low tariff + # and tariff code 2 is used for normal tariff. + + if value == '0002': + return 'normal' + elif value == '0001': + return 'low' + else: + return None diff --git a/requirements_all.txt b/requirements_all.txt index c5257dd0dde..7d123c93b77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,6 +174,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 +# homeassistant.components.sensor.dsmr +https://github.com/aequitas/dsmr_parser/archive/async_protocol.zip#dsmr_parser==0.4 + # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.6.zip#braviarc==0.3.6 diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py new file mode 100644 index 00000000000..166a4af9657 --- /dev/null +++ b/tests/components/sensor/test_dsmr.py @@ -0,0 +1,64 @@ +"""Test for DSMR components. + +Tests setup of the DSMR component and ensure incoming telegrams cause Entity +to be updated with new values. +""" + +import asyncio +from decimal import Decimal +from unittest.mock import Mock + +from homeassistant.bootstrap import async_setup_component +from tests.common import assert_setup_component + + +@asyncio.coroutine +def test_default_setup(hass, monkeypatch): + """Test the default setup.""" + from dsmr_parser.obis_references import ( + CURRENT_ELECTRICITY_USAGE, + ELECTRICITY_ACTIVE_TARIFF, + ) + from dsmr_parser.objects import CosemObject + + config = {'platform': 'dsmr'} + + telegram = { + CURRENT_ELECTRICITY_USAGE: CosemObject([ + {'value': Decimal('0.1'), 'unit': 'kWh'} + ]), + ELECTRICITY_ACTIVE_TARIFF: CosemObject([ + {'value': '0001', 'unit': ''} + ]), + } + + # mock for injecting DSMR telegram + dsmr = Mock(return_value=Mock()) + monkeypatch.setattr('dsmr_parser.protocol.create_dsmr_reader', dsmr) + + with assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', + {'sensor': config}) + + telegram_callback = dsmr.call_args_list[0][0][2] + + # make sure entities have been created and return 'unknown' state + power_consumption = hass.states.get('sensor.power_consumption') + assert power_consumption.state == 'unknown' + assert power_consumption.attributes.get('unit_of_measurement') is None + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + yield from asyncio.sleep(0, loop=hass.loop) + + # ensure entities have new state value after incoming telegram + power_consumption = hass.states.get('sensor.power_consumption') + assert power_consumption.state == '0.1' + assert power_consumption.attributes.get('unit_of_measurement') is 'kWh' + + # tariff should be translated in human readable and have no unit + power_tariff = hass.states.get('sensor.power_tariff') + assert power_tariff.state == 'low' + assert power_tariff.attributes.get('unit_of_measurement') is None From c9b353f7a7e799d7cce338f924e290507e63766d Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Wed, 23 Nov 2016 08:22:52 +0100 Subject: [PATCH 040/137] Add Bose SoundTouch device support - v2 (#4523) * Add Bose SoundTouch device support * Update soundtouch.py --- .../components/media_player/services.yaml | 33 + .../components/media_player/soundtouch.py | 393 +++++++++++ requirements_all.txt | 3 + .../media_player/test_soundtouch.py | 652 ++++++++++++++++++ 4 files changed, 1081 insertions(+) create mode 100644 homeassistant/components/media_player/soundtouch.py create mode 100644 tests/components/media_player/test_soundtouch.py diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index ee0225d2a76..9482661b464 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -204,3 +204,36 @@ sonos_clear_sleep_timer: entity_id: description: Name(s) of entites that will have the timer cleared. example: 'media_player.living_room_sonos' + + +soundtouch_play_everywhere: + description: Play on all Bose Soundtouch devices + + fields: + entity_id: + description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices + example: 'media_player.soundtouch_home' + +soundtouch_create_zone: + description: Create a multi-room zone + + fields: + entity_id: + description: Name of entites that will coordinate the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + +soundtouch_add_zone_slave: + description: Add a slave to a multi-room zone + + fields: + entity_id: + description: Name of entites that will be added to the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + +soundtouch_remove_zone_slave: + description: Remove a slave from the multi-room zone + + fields: + entity_id: + description: Name of entites that will be remove from the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' \ No newline at end of file diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py new file mode 100644 index 00000000000..c33422f871e --- /dev/null +++ b/homeassistant/components/media_player/soundtouch.py @@ -0,0 +1,393 @@ +"""Support for interface with a Bose Soundtouch.""" +import logging + +from os import path +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT, + STATE_PAUSED, STATE_PLAYING, + STATE_UNAVAILABLE) + +REQUIREMENTS = ['libsoundtouch==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'media_player' +SERVICE_PLAY_EVERYWHERE = 'soundtouch_play_everywhere' +SERVICE_CREATE_ZONE = 'soundtouch_create_zone' +SERVICE_ADD_ZONE_SLAVE = 'soundtouch_add_zone_slave' +SERVICE_REMOVE_ZONE_SLAVE = 'soundtouch_remove_zone_slave' + +MAP_STATUS = { + "PLAY_STATE": STATE_PLAYING, + "BUFFERING_STATE": STATE_PLAYING, + "PAUSE_STATE": STATE_PAUSED, + "STOp_STATE": STATE_OFF +} + +SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({ + 'master': cv.entity_id, +}) + +SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({ + 'master': cv.entity_id, + 'slaves': cv.entity_ids +}) + +SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({ + 'master': cv.entity_id, + 'slaves': cv.entity_ids +}) + +SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({ + 'master': cv.entity_id, + 'slaves': cv.entity_ids +}) + +DEFAULT_NAME = 'Bose Soundtouch' +DEFAULT_PORT = 8090 + +DEVICES = [] + +SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ + SUPPORT_VOLUME_SET | SUPPORT_TURN_ON + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Bose Soundtouch platform.""" + name = config.get(CONF_NAME) + + remote_config = { + 'name': 'HomeAssistant', + 'description': config.get(CONF_NAME), + 'id': 'ha.component.soundtouch', + 'port': config.get(CONF_PORT), + 'host': config.get(CONF_HOST) + } + + soundtouch_device = SoundTouchDevice(name, remote_config) + DEVICES.append(soundtouch_device) + add_devices([soundtouch_device]) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE, + play_everywhere_service, + descriptions.get(SERVICE_PLAY_EVERYWHERE), + schema=SOUNDTOUCH_PLAY_EVERYWHERE) + hass.services.register(DOMAIN, SERVICE_CREATE_ZONE, + create_zone_service, + descriptions.get(SERVICE_CREATE_ZONE), + schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA) + hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, + remove_zone_slave, + descriptions.get(SERVICE_REMOVE_ZONE_SLAVE), + schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA) + hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE, + add_zone_slave, + descriptions.get(SERVICE_ADD_ZONE_SLAVE), + schema=SOUNDTOUCH_ADD_ZONE_SCHEMA) + + +def play_everywhere_service(service): + """ + Create a zone (multi-room) and play on all devices. + + :param service: Home Assistant service with 'master' data set + + :Example: + + - service: media_player.soundtouch_play_everywhere + data: + master: media_player.soundtouch_living_room + + """ + master_device_id = service.data.get('master') + slaves = [d for d in DEVICES if d.entity_id != master_device_id] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + str(master_device_id)) + elif not slaves: + _LOGGER.warning("Unable to create zone without slaves") + else: + _LOGGER.info( + "Creating zone with master " + str(master.device.config.name)) + master.device.create_zone([slave.device for slave in slaves]) + + +def create_zone_service(service): + """ + Create a zone (multi-room) on a master and play on specified slaves. + + At least one master and one slave must be specified + + :param service: Home Assistant service with 'master' and 'slaves' data set + + :Example: + + - service: media_player.soundtouch_create_zone + data: + master: media_player.soundtouch_living_room + slaves: + - media_player.soundtouch_room + - media_player.soundtouch_kitchen + + """ + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [device for device in DEVICES if device.entity_id in slaves_ids] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + master_device_id) + elif not slaves: + _LOGGER.warning("Unable to create zone without slaves") + else: + _LOGGER.info( + "Creating zone with master " + str(master.device.config.name)) + master.device.create_zone([slave.device for slave in slaves]) + + +def add_zone_slave(service): + """ + Add slave(s) to and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + + :param service: Home Assistant service with 'master' and 'slaves' data set + + :Example: + + - service: media_player.soundtouch_add_zone_slave + data: + master: media_player.soundtouch_living_room + slaves: + - media_player.soundtouch_room + + """ + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [device for device in DEVICES if device.entity_id in slaves_ids] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + str(master_device_id)) + elif not slaves: + _LOGGER.warning("Unable to find slaves to add") + else: + _LOGGER.info( + "Adding slaves to zone with master " + str( + master.device.config.name)) + master.device.add_zone_slave([slave.device for slave in slaves]) + + +def remove_zone_slave(service): + """ + Remove slave(s) from and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + Note: If removing last slave, the zone will be deleted and you'll have to + create a new one. You will not be able to add a new slave anymore + + :param service: Home Assistant service with 'master' and 'slaves' data set + + :Example: + + - service: media_player.soundtouch_remove_zone_slave + data: + master: media_player.soundtouch_living_room + slaves: + - media_player.soundtouch_room + + """ + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [device for device in DEVICES if device.entity_id in slaves_ids] + master = next([device for device in DEVICES if + device.entity_id == master_device_id].__iter__(), None) + if master is None: + _LOGGER.warning( + "Unable to find master with entity_id:" + master_device_id) + elif not slaves: + _LOGGER.warning("Unable to find slaves to remove") + else: + _LOGGER.info("Removing slaves from zone with master " + + str(master.device.config.name)) + master.device.remove_zone_slave([slave.device for slave in slaves]) + + +class SoundTouchDevice(MediaPlayerDevice): + """Representation of a SoundTouch Bose device.""" + + def __init__(self, name, config): + """Create Soundtouch Entity.""" + from libsoundtouch import soundtouch_device + self._name = name + self._device = soundtouch_device(config['host'], config['port']) + self._status = self._device.status() + self._volume = self._device.volume() + self._config = config + + @property + def config(self): + """Return specific soundtouch configuration.""" + return self._config + + @property + def device(self): + """Return Soundtouch device.""" + return self._device + + def update(self): + """Retrieve the latest data.""" + self._status = self._device.status() + self._volume = self._device.volume() + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume.actual / 100 + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._status.source == 'STANDBY': + return STATE_OFF + else: + return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._volume.muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_SOUNDTOUCH + + def turn_off(self): + """Turn off media player.""" + self._device.power_off() + self._status = self._device.status() + + def turn_on(self): + """Turn the media player on.""" + self._device.power_on() + self._status = self._device.status() + + def volume_up(self): + """Volume up the media player.""" + self._device.volume_up() + self._volume = self._device.volume() + + def volume_down(self): + """Volume down media player.""" + self._device.volume_down() + self._volume = self._device.volume() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._device.set_volume(int(volume * 100)) + self._volume = self._device.volume() + + def mute_volume(self, mute): + """Send mute command.""" + self._device.mute() + self._volume = self._device.volume() + + def media_play_pause(self): + """Simulate play pause media player.""" + self._device.play_pause() + self._status = self._device.status() + + def media_play(self): + """Send play command.""" + self._device.play() + self._status = self._device.status() + + def media_pause(self): + """Send media pause command to media player.""" + self._device.pause() + self._status = self._device.status() + + def media_next_track(self): + """Send next track command.""" + self._device.next_track() + self._status = self._device.status() + + def media_previous_track(self): + """Send the previous track command.""" + self._device.previous_track() + self._status = self._device.status() + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._status.image + + @property + def media_title(self): + """Title of current playing media.""" + if self._status.station_name is not None: + return self._status.station_name + elif self._status.artist is not None: + return self._status.artist + " - " + self._status.track + else: + return None + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._status.duration + + @property + def media_artist(self): + """Artist of current playing media.""" + return self._status.artist + + @property + def media_track(self): + """Artist of current playing media.""" + return self._status.track + + @property + def media_album_name(self): + """Album name of current playing media.""" + return self._status.album + + def play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + _LOGGER.info("Starting media with media_id:" + str(media_id)) + presets = self._device.presets() + preset = next([preset for preset in presets if + preset.preset_id == str(media_id)].__iter__(), None) + if preset is not None: + _LOGGER.info("Playing preset: " + preset.name) + self._device.select_preset(preset) + else: + _LOGGER.warning("Unable to find preset with id " + str(media_id)) diff --git a/requirements_all.txt b/requirements_all.txt index 7d123c93b77..6993c777561 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -261,6 +261,9 @@ knxip==0.3.3 # homeassistant.components.device_tracker.owntracks libnacl==1.5.0 +# homeassistant.components.media_player.soundtouch +libsoundtouch==0.1.0 + # homeassistant.components.light.lifx liffylights==0.9.4 diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py new file mode 100644 index 00000000000..b95b774845a --- /dev/null +++ b/tests/components/media_player/test_soundtouch.py @@ -0,0 +1,652 @@ +"""Test the Soundtouch component.""" +import logging +import unittest +from unittest import mock +from libsoundtouch.device import SoundTouchDevice as STD, Status, Volume, \ + Preset, Config + +from homeassistant.components.media_player import soundtouch +from homeassistant.const import ( + STATE_OFF, STATE_PAUSED, STATE_PLAYING) +from tests.common import get_test_home_assistant + + +class MockService: + """Mock Soundtouch service.""" + + def __init__(self, master, slaves): + """Create a new service.""" + self.data = { + "master": master, + "slaves": slaves + } + + +def _mock_soundtouch_device(*args, **kwargs): + return MockDevice() + + +class MockDevice(STD): + """Mock device.""" + + def __init__(self): + self._config = MockConfig + + +class MockConfig(Config): + """Mock config.""" + + def __init__(self): + self._name = "name" + + +def _mocked_presets(*args, **kwargs): + """Return a list of mocked presets.""" + return [MockPreset("1")] + + +class MockPreset(Preset): + """Mock preset.""" + + def __init__(self, id): + self._id = id + self._name = "preset" + + +class MockVolume(Volume): + """Mock volume with value.""" + + def __init__(self): + self._actual = 12 + + +class MockVolumeMuted(Volume): + """Mock volume muted.""" + + def __init__(self): + self._actual = 12 + self._muted = True + + +class MockStatusStandby(Status): + """Mock status standby.""" + + def __init__(self): + self._source = "STANDBY" + + +class MockStatusPlaying(Status): + """Mock status playing media.""" + + def __init__(self): + self._source = "" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = "artist" + self._track = "track" + self._album = "album" + self._duration = 1 + self._station_name = None + + +class MockStatusPlayingRadio(Status): + """Mock status radio.""" + + def __init__(self): + self._source = "" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = None + self._track = None + self._album = None + self._duration = None + self._station_name = "station" + + +class MockStatusUnknown(Status): + """Mock status unknown media.""" + + def __init__(self): + self._source = "" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = None + self._track = None + self._album = None + self._duration = None + self._station_name = None + + +class MockStatusPause(Status): + """Mock status pause.""" + + def __init__(self): + self._source = "" + self._play_status = "PAUSE_STATE" + + +def default_component(): + """Return a default component.""" + return { + 'host': '192.168.0.1', + 'port': 8090, + 'name': 'soundtouch' + } + + +class TestSoundtouchMediaPlayer(unittest.TestCase): + """Bose Soundtouch test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + logging.disable(logging.CRITICAL) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + logging.disable(logging.NOTSET) + soundtouch.DEVICES = [] + self.hass.stop() + + @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) + def test_ensure_setup_config(self, mocked_sountouch_device): + """Test setup OK.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + # soundtouch.DEVICES[0].entity_id = 'entity_1' + self.assertEqual(len(soundtouch.DEVICES), 1) + self.assertEqual(soundtouch.DEVICES[0].name, 'soundtouch') + self.assertEqual(soundtouch.DEVICES[0].config['port'], 8090) + self.assertEqual(mocked_sountouch_device.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_update(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test update device state.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + soundtouch.DEVICES[0].update() + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusPlaying) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_playing_media(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test playing media info.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_PLAYING) + self.assertEqual(soundtouch.DEVICES[0].media_image_url, "image.url") + self.assertEqual(soundtouch.DEVICES[0].media_title, "artist - track") + self.assertEqual(soundtouch.DEVICES[0].media_track, "track") + self.assertEqual(soundtouch.DEVICES[0].media_artist, "artist") + self.assertEqual(soundtouch.DEVICES[0].media_album_name, "album") + self.assertEqual(soundtouch.DEVICES[0].media_duration, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusUnknown) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_playing_unknown_media(self, mocked_sountouch_device, + mocked_status, mocked_volume): + """Test playing media info.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].media_title, None) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusPlayingRadio) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_playing_radio(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test playing radio info.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_PLAYING) + self.assertEqual(soundtouch.DEVICES[0].media_image_url, "image.url") + self.assertEqual(soundtouch.DEVICES[0].media_title, "station") + self.assertEqual(soundtouch.DEVICES[0].media_track, None) + self.assertEqual(soundtouch.DEVICES[0].media_artist, None) + self.assertEqual(soundtouch.DEVICES[0].media_album_name, None) + self.assertEqual(soundtouch.DEVICES[0].media_duration, None) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', + side_effect=MockVolume) + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_get_volume_level(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test volume level.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].volume_level, 0.12) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusStandby) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_get_state_off(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test state device is off.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_OFF) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status', + side_effect=MockStatusPause) + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_get_state_pause(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test state device is paused.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].state, STATE_PAUSED) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', + side_effect=MockVolumeMuted) + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_is_muted(self, mocked_sountouch_device, mocked_status, + mocked_volume): + """Test device volume is muted.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].is_volume_muted, True) + + @mock.patch('libsoundtouch.soundtouch_device') + def test_media_commands(self, mocked_sountouch_device): + """Test supported media commands.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(soundtouch.DEVICES[0].supported_media_commands, 1469) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_should_turn_off(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_power_off): + """Test device is turned off.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].turn_off() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_power_off.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.power_on') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_should_turn_on(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_power_on): + """Test device is turned on.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].turn_on() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_power_on.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume_up') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_volume_up(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_volume_up): + """Test volume up.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].volume_up() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + self.assertEqual(mocked_volume_up.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume_down') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_volume_down(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_volume_down): + """Test volume down.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].volume_down() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + self.assertEqual(mocked_volume_down.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.set_volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_set_volume_level(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_set_volume): + """Test set volume level.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].set_volume_level(0.17) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + mocked_set_volume.assert_called_with(17) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.mute') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_mute(self, mocked_sountouch_device, mocked_status, mocked_volume, + mocked_mute): + """Test mute volume.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].mute_volume(None) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 2) + self.assertEqual(mocked_mute.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.play') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play(self, mocked_sountouch_device, mocked_status, mocked_volume, + mocked_play): + """Test play command.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].media_play() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_play.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.pause') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_pause(self, mocked_sountouch_device, mocked_status, mocked_volume, + mocked_pause): + """Test pause command.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].media_pause() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_pause.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.play_pause') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play_pause_play(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_play_pause): + """Test play/pause.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].media_play_pause() + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 1) + self.assertEqual(mocked_play_pause.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.previous_track') + @mock.patch('libsoundtouch.device.SoundTouchDevice.next_track') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_next_previous_track(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_next_track, + mocked_previous_track): + """Test next/previous track.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + soundtouch.DEVICES[0].media_next_track() + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_next_track.call_count, 1) + soundtouch.DEVICES[0].media_previous_track() + self.assertEqual(mocked_status.call_count, 3) + self.assertEqual(mocked_previous_track.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.select_preset') + @mock.patch('libsoundtouch.device.SoundTouchDevice.presets', + side_effect=_mocked_presets) + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play_media(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_presets, mocked_select_preset): + """Test play preset 1.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(mocked_sountouch_device.call_count, 1) + self.assertEqual(mocked_status.call_count, 1) + self.assertEqual(mocked_volume.call_count, 1) + soundtouch.DEVICES[0].play_media('PLAYLIST', 1) + self.assertEqual(mocked_presets.call_count, 1) + self.assertEqual(mocked_select_preset.call_count, 1) + soundtouch.DEVICES[0].play_media('PLAYLIST', 2) + self.assertEqual(mocked_presets.call_count, 2) + self.assertEqual(mocked_select_preset.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_play_everywhere(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_create_zone): + """Test play everywhere.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # one master, one slave => create zone + service = MockService("entity_1", []) + soundtouch.play_everywhere_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # unknown master. create zone is must not be called + service = MockService("entity_X", []) + soundtouch.play_everywhere_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # no slaves, create zone must not be called + soundtouch.DEVICES.pop(1) + service = MockService("entity_1", []) + soundtouch.play_everywhere_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_create_zone(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_create_zone): + """Test creating a zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # one master, one slave => create zone + service = MockService("entity_1", ["entity_2"]) + soundtouch.create_zone_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # unknown master. create zone is must not be called + service = MockService("entity_X", []) + soundtouch.create_zone_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + # no slaves, create zone must not be called + soundtouch.DEVICES.pop(1) + service = MockService("entity_1", []) + soundtouch.create_zone_service(service) + self.assertEqual(mocked_create_zone.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.add_zone_slave') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_add_zone_slave(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_add_zone_slave): + """Test adding a slave to an existing zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # remove one slave + service = MockService("entity_1", ["entity_2"]) + soundtouch.add_zone_slave(service) + self.assertEqual(mocked_add_zone_slave.call_count, 1) + + # unknown master. add zone slave is not called + service = MockService("entity_X", ["entity_2"]) + soundtouch.add_zone_slave(service) + self.assertEqual(mocked_add_zone_slave.call_count, 1) + + # no slave to add, add zone slave is not called + service = MockService("entity_1", []) + soundtouch.add_zone_slave(service) + self.assertEqual(mocked_add_zone_slave.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.remove_zone_slave') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_remove_zone_slave): + """Test removing a slave from a zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.DEVICES[0].entity_id = "entity_1" + soundtouch.DEVICES[1].entity_id = "entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # remove one slave + service = MockService("entity_1", ["entity_2"]) + soundtouch.remove_zone_slave(service) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # unknown master. remove zone slave is not called + service = MockService("entity_X", ["entity_2"]) + soundtouch.remove_zone_slave(service) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # no slave to add, add zone slave is not called + service = MockService("entity_1", []) + soundtouch.remove_zone_slave(service) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) From c22a73e1d0d58ee2660caf62d4d71a651a3019d8 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Wed, 23 Nov 2016 02:41:51 -0500 Subject: [PATCH 041/137] Removed raise statement to don't pollute the user log. (#4536) * Removed raise statement to don't polute the user log. Only the error message should be displayed. Nov 22 11:28:32 tchellopi hass[20138]: 16-11-22 11:28:32 ERROR (MainThread) [homeassistant.core] Error doing job: Task exception was never retrieved Nov 22 11:28:32 tchellopi hass[20138]: Traceback (most recent call last): Nov 22 11:28:32 tchellopi hass[20138]: File "/usr/local/lib/python3.5/asyncio/tasks.py", line 241, in _step Nov 22 11:28:32 tchellopi hass[20138]: result = coro.throw(exc) Nov 22 11:28:32 tchellopi hass[20138]: File "/home/hass/.virtualenvs/home_assistant/lib/python3.5/site-packages/homeassistant/helpers/entity_component.py", line 386, in _update_entity_states Nov 22 11:28:32 tchellopi hass[20138]: yield from update_coro Nov 22 11:28:32 tchellopi hass[20138]: File "/home/hass/.virtualenvs/home_assistant/lib/python3.5/site-packages/homeassistant/helpers/entity.py", line 213, in async_update_ha_state Nov 22 11:28:32 tchellopi hass[20138]: yield from self.hass.loop.run_in_executor(None, self.update) Nov 22 11:28:32 tchellopi hass[20138]: File "/usr/local/lib/python3.5/asyncio/futures.py", line 361, in __iter__ Nov 22 11:28:32 tchellopi hass[20138]: yield self # This tells Task to wait for completion. Nov 22 11:28:32 tchellopi hass[20138]: File "/usr/local/lib/python3.5/asyncio/tasks.py", line 296, in _wakeup Nov 22 11:28:32 tchellopi hass[20138]: future.result() Nov 22 11:28:32 tchellopi hass[20138]: File "/usr/local/lib/python3.5/asyncio/futures.py", line 274, in result Nov 22 11:28:32 tchellopi hass[20138]: raise self._exception Nov 22 11:28:32 tchellopi hass[20138]: File "/usr/local/lib/python3.5/concurrent/futures/thread.py", line 55, in run Nov 22 11:28:32 tchellopi hass[20138]: result = self.fn(*self.args, **self.kwargs) Nov 22 11:28:32 tchellopi hass[20138]: File "/home/hass/.homeassistant/custom_components/sensor/wunderground.py", line 187, in update Nov 22 11:28:32 tchellopi hass[20138]: self.rest.update() Nov 22 11:28:32 tchellopi hass[20138]: File "/home/hass/.virtualenvs/home_assistant/lib/python3.5/site-packages/homeassistant/util/__init__.py", line 296, in wrapper Nov 22 11:28:32 tchellopi hass[20138]: result = method(*args, **kwargs) Nov 22 11:28:32 tchellopi hass[20138]: File "/home/hass/.homeassistant/custom_components/sensor/wunderground.py", line 222, in update Nov 22 11:28:32 tchellopi hass[20138]: ["description"]) Nov 22 11:28:32 tchellopi hass[20138]: ValueError: you must supply a key * Updated unittest since we are just printing the error instead raising --- homeassistant/components/sensor/wunderground.py | 2 -- tests/components/sensor/test_wunderground.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 512a937650f..82bb3d3b245 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -222,7 +222,6 @@ class WUndergroundData(object): except ValueError as err: _LOGGER.error("Check WUnderground API %s", err.args) self.data = None - raise @Throttle(MIN_TIME_BETWEEN_UPDATES_ALERTS) def update_alerts(self): @@ -237,4 +236,3 @@ class WUndergroundData(object): except ValueError as err: _LOGGER.error("Check WUnderground API %s", err.args) self.alerts = None - raise diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index fb76b93885a..cde94558866 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -137,7 +137,7 @@ class TestWundergroundSetup(unittest.TestCase): ] } - self.assertFalse( + self.assertTrue( wunderground.setup_platform(self.hass, invalid_config, self.add_devices, None)) From 05181bf232cceb43986410431b9144daeaa8926e Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Wed, 23 Nov 2016 11:44:37 +0100 Subject: [PATCH 042/137] 0.4 release upstream. (#4545) --- homeassistant/components/sensor/dsmr.py | 5 +---- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index eb8e5174b47..2e9b8b8652c 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -39,10 +39,7 @@ from homeassistant.helpers.entity import Entity DOMAIN = 'dsmr' -REQUIREMENTS = [ - 'https://github.com/aequitas/dsmr_parser/archive/async_protocol.zip' - '#dsmr_parser==0.4' -] +REQUIREMENTS = ['dsmr_parser==0.4'] # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) diff --git a/requirements_all.txt b/requirements_all.txt index 6993c777561..af47a1daac9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,6 +84,9 @@ dnspython3==1.15.0 # homeassistant.components.sensor.dovado dovado==0.1.15 +# homeassistant.components.sensor.dsmr +dsmr_parser==0.4 + # homeassistant.components.dweet # homeassistant.components.sensor.dweet dweepy==0.2.0 @@ -174,9 +177,6 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 -# homeassistant.components.sensor.dsmr -https://github.com/aequitas/dsmr_parser/archive/async_protocol.zip#dsmr_parser==0.4 - # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.6.zip#braviarc==0.3.6 From 5013a826550d1acc451212fad09f01b5cabb2040 Mon Sep 17 00:00:00 2001 From: dasos Date: Wed, 23 Nov 2016 14:52:14 +0000 Subject: [PATCH 043/137] Hook Smart Home support (#4392) * Support for Hook (hooksmarthome.com) * Linting * Add asyncio * Move to aiohttp * Yield more --- .coveragerc | 1 + homeassistant/components/switch/hook.py | 137 ++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 homeassistant/components/switch/hook.py diff --git a/.coveragerc b/.coveragerc index 0840e5bcb4d..7b7b063edb0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -317,6 +317,7 @@ omit = homeassistant/components/switch/dlink.py homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py + homeassistant/components/switch/hook.py homeassistant/components/switch/mystrom.py homeassistant/components/switch/netio.py homeassistant/components/switch/orvibo.py diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py new file mode 100644 index 00000000000..9bbb168a099 --- /dev/null +++ b/homeassistant/components/switch/hook.py @@ -0,0 +1,137 @@ +""" +Support Hook, available at hooksmarthome.com. + +Controls RF switches like these: + https://www.amazon.com/Etekcity-Wireless-Electrical-Household-Appliances/dp/B00DQELHBS + +There is no way to query for state or success of commands. + +""" +import logging +import asyncio +import voluptuous as vol +import async_timeout +import aiohttp + +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +HOOK_ENDPOINT = "https://api.gethook.io/v1/" +TIMEOUT = 10 + +SWITCH_SCHEMA = vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup Hook by getting the access token and list of actions.""" + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + response = yield from hass.websession.post( + HOOK_ENDPOINT + 'user/login', + data={ + 'username': username, + 'password': password}) + data = yield from response.json() + except (asyncio.TimeoutError, + aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as error: + _LOGGER.error("Failed authentication API call: %s", error) + return False + + try: + token = data['data']['token'] + except KeyError: + _LOGGER.error("No token. Check username and password") + return False + + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + response = yield from hass.websession.get( + HOOK_ENDPOINT + 'device', + params={"token": data['data']['token']}) + data = yield from response.json() + except (asyncio.TimeoutError, + aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as error: + _LOGGER.error("Failed getting devices: %s", error) + return False + + yield from async_add_devices( + HookSmartHome( + hass, + token, + d['device_id'], + d['device_name']) + for lst in data['data'] + for d in lst) + + +class HookSmartHome(SwitchDevice): + """Representation of a Hook device, allowing on and off commands.""" + + # pylint: disable=too-many-arguments + def __init__(self, hass, token, device_id, device_name): + """Initialize the switch.""" + self._hass = hass + self._token = token + self._state = False + self._id = device_id + self._name = device_name + _LOGGER.debug( + "Creating Hook object: ID: " + self._id + + " Name: " + self._name) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @asyncio.coroutine + def _send(self, url): + """Send the url to the Hook API.""" + try: + _LOGGER.debug("Sending: %s", url) + with async_timeout.timeout(TIMEOUT, loop=self._hass.loop): + response = yield from self._hass.websession.get( + url, + params={"token": self._token}) + data = yield from response.json() + except (asyncio.TimeoutError, + aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as error: + _LOGGER.error("Failed setting state: %s", error) + return False + _LOGGER.debug("Got: %s", data) + return data['return_value'] == '1' + + @asyncio.coroutine + def async_turn_on(self): + """Turn the device on asynchronously.""" + _LOGGER.debug("Turning on: %s", self._name) + success = yield from self._send( + HOOK_ENDPOINT + 'device/trigger/' + self._id + '/On') + self._state = success + + @asyncio.coroutine + def async_turn_off(self): + """Turn the device off asynchronously.""" + _LOGGER.debug("Turning off: %s", self._name) + success = yield from self._send( + HOOK_ENDPOINT + 'device/trigger/' + self._id + '/Off') + # If it wasn't successful, keep state as true + self._state = not success From c04a002c5580b4eb530977b40a37559cbaa25ff2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 23 Nov 2016 18:52:03 +0100 Subject: [PATCH 044/137] Hotfix executor pool size (#4552) --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index f7847228338..50c805e2548 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -109,7 +109,7 @@ class HomeAssistant(object): else: self.loop = loop or asyncio.get_event_loop() - self.executor = ThreadPoolExecutor(max_workers=5) + self.executor = ThreadPoolExecutor(max_workers=EXECUTOR_POOL_SIZE) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) self._pending_tasks = [] From 475c412ae4e50544602bd799cebfee4fc3c2d3b8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 24 Nov 2016 00:21:48 +0100 Subject: [PATCH 045/137] Minor changes (switch.hook) (#4553) * Use string formatting, add link to docs, and pylint * Extent platform for validation --- homeassistant/components/switch/hook.py | 39 ++++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 9bbb168a099..8f24842212d 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -1,43 +1,41 @@ """ Support Hook, available at hooksmarthome.com. -Controls RF switches like these: - https://www.amazon.com/Etekcity-Wireless-Electrical-Household-Appliances/dp/B00DQELHBS - -There is no way to query for state or success of commands. - +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.hook/ """ import logging import asyncio + import voluptuous as vol import async_timeout import aiohttp -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -HOOK_ENDPOINT = "https://api.gethook.io/v1/" +HOOK_ENDPOINT = 'https://api.gethook.io/v1/' TIMEOUT = 10 -SWITCH_SCHEMA = vol.Schema({ +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup Hook by getting the access token and list of actions.""" + """Set up Hook by getting the access token and list of actions.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): response = yield from hass.websession.post( - HOOK_ENDPOINT + 'user/login', + '{}{}'.format(HOOK_ENDPOINT, 'user/login'), data={ 'username': username, 'password': password}) @@ -57,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): response = yield from hass.websession.get( - HOOK_ENDPOINT + 'device', + '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": data['data']['token']}) data = yield from response.json() except (asyncio.TimeoutError, @@ -79,7 +77,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class HookSmartHome(SwitchDevice): """Representation of a Hook device, allowing on and off commands.""" - # pylint: disable=too-many-arguments def __init__(self, hass, token, device_id, device_name): """Initialize the switch.""" self._hass = hass @@ -88,8 +85,7 @@ class HookSmartHome(SwitchDevice): self._id = device_id self._name = device_name _LOGGER.debug( - "Creating Hook object: ID: " + self._id + - " Name: " + self._name) + "Creating Hook object: ID: %s Name: %s", self._id, self._name) @property def name(self): @@ -108,8 +104,7 @@ class HookSmartHome(SwitchDevice): _LOGGER.debug("Sending: %s", url) with async_timeout.timeout(TIMEOUT, loop=self._hass.loop): response = yield from self._hass.websession.get( - url, - params={"token": self._token}) + url, params={"token": self._token}) data = yield from response.json() except (asyncio.TimeoutError, aiohttp.errors.ClientError, @@ -123,15 +118,17 @@ class HookSmartHome(SwitchDevice): def async_turn_on(self): """Turn the device on asynchronously.""" _LOGGER.debug("Turning on: %s", self._name) - success = yield from self._send( - HOOK_ENDPOINT + 'device/trigger/' + self._id + '/On') + url = '{}{}{}{}'.format( + HOOK_ENDPOINT, 'device/trigger/', self._id, '/On') + success = yield from self._send(url) self._state = success @asyncio.coroutine def async_turn_off(self): """Turn the device off asynchronously.""" _LOGGER.debug("Turning off: %s", self._name) - success = yield from self._send( - HOOK_ENDPOINT + 'device/trigger/' + self._id + '/Off') + url = '{}{}{}{}'.format( + HOOK_ENDPOINT, 'device/trigger/', self._id, '/Off') + success = yield from self._send(url) # If it wasn't successful, keep state as true self._state = not success From b6d559da1ff2224952ea03bcb09a986a6879686f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 24 Nov 2016 00:26:59 +0100 Subject: [PATCH 046/137] Add timeout to requests, use consts, and add link to docs (#4555) --- homeassistant/components/sensor/sonarr.py | 72 +++++++++-------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py index 0023755bc04..9f4ed3d0581 100644 --- a/homeassistant/components/sensor/sonarr.py +++ b/homeassistant/components/sensor/sonarr.py @@ -1,23 +1,29 @@ -"""Support for Sonarr.""" +""" +Support for Sonarr. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sonarr/ +""" import logging import time from datetime import datetime + import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_API_KEY +from homeassistant.const import (CONF_API_KEY, CONF_HOST, CONF_PORT) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_SSL from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA + _LOGGER = logging.getLogger(__name__) -CONF_HOST = 'host' -CONF_PORT = 'port' CONF_DAYS = 'days' CONF_INCLUDED = 'include_paths' CONF_UNIT = 'unit' + DEFAULT_HOST = 'localhost' DEFAULT_PORT = 8989 DEFAULT_DAYS = '1' @@ -41,7 +47,7 @@ ENDPOINTS = { 'commands': 'http{0}://{1}:{2}/api/command?apikey={3}' } -# Suport to Yottabytes for the future, why not +# Support to Yottabytes for the future, why not BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -57,21 +63,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Sonarr platform.""" + """Set up the Sonarr platform.""" conditions = config.get(CONF_MONITORED_CONDITIONS) add_devices( - [Sonarr(hass, config, sensor) for sensor in conditions] + [SonarrSensor(hass, config, sensor) for sensor in conditions] ) return True -class Sonarr(Entity): - """Implement the Sonarr sensor class.""" +class SonarrSensor(Entity): + """Implemention of the Sonarr sensor.""" def __init__(self, hass, conf, sensor_type): - """Create sonarr entity.""" + """Create Sonarr entity.""" from pytz import timezone - # Configuration data self.conf = conf self.host = conf.get(CONF_HOST) self.port = conf.get(CONF_PORT) @@ -99,19 +104,13 @@ class Sonarr(Entity): end = get_date(self._tz, self.days) res = requests.get( ENDPOINTS[self.type].format( - self.ssl, - self.host, - self.port, - self.apikey, - start, - end - ) - ) + self.ssl, self.host, self.port, self.apikey, start, end), + timeout=5) if res.status_code == 200: if self.type in ['upcoming', 'queue', 'series', 'commands']: if self.days == 1 and self.type == 'upcoming': - # Sonarr API returns empty array if start and end dates are - # the same, so we need to filter to just today + # Sonarr API returns an empty array if start and end dates + # are the same, so we need to filter to just today self.data = list( filter( lambda x: x['airDate'] == str(start), @@ -125,13 +124,8 @@ class Sonarr(Entity): data = res.json() res = requests.get('{}&pageSize={}'.format( ENDPOINTS[self.type].format( - self.ssl, - self.host, - self.port, - self.apikey - ), - data['totalRecords'] - )) + self.ssl, self.host, self.port, self.apikey), + data['totalRecords']), timeout=5) self.data = res.json()['records'] self._state = len(self.data) elif self.type == 'diskspace': @@ -156,7 +150,7 @@ class Sonarr(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format("Sonarr", self._name) + return '{} {}'.format('Sonarr', self._name) @property def state(self): @@ -187,8 +181,7 @@ class Sonarr(Entity): elif self.type == 'wanted': for show in self.data: attributes[show['series']['title'] + ' S{:02d}E{:02d}'.format( - show['seasonNumber'], - show['episodeNumber'] + show['seasonNumber'], show['episodeNumber'] )] = show['airDate'] elif self.type == 'commands': for command in self.data: @@ -198,16 +191,9 @@ class Sonarr(Entity): attributes[data['path']] = '{:.2f}/{:.2f}{} ({:.2f}%)'.format( to_unit(data['freeSpace'], self._unit), to_unit(data['totalSpace'], self._unit), - self._unit, - ( - to_unit( - data['freeSpace'], - self._unit - ) / - to_unit( - data['totalSpace'], - self._unit - )*100 + self._unit, ( + to_unit(data['freeSpace'], self._unit) / + to_unit(data['totalSpace'], self._unit) * 100 ) ) elif self.type == 'series': @@ -226,7 +212,7 @@ class Sonarr(Entity): def get_date(zone, offset=0): """Get date based on timezone and offset of days.""" - day = 60*60*24 + day = 60 * 60 * 24 return datetime.date( datetime.fromtimestamp(time.time() + day*offset, tz=zone) ) @@ -234,4 +220,4 @@ def get_date(zone, offset=0): def to_unit(value, unit): """Convert bytes to give unit.""" - return value/1024**BYTE_SIZES.index(unit) + return value / 1024**BYTE_SIZES.index(unit) From b1b8715f7dbf7fb5a69d69642030215388b3295c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 24 Nov 2016 00:27:31 +0100 Subject: [PATCH 047/137] Minor comment updates and ordering (#4554) --- homeassistant/components/sensor/dsmr.py | 47 +++++++++++-------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 2e9b8b8652c..8d27f7188d2 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -25,7 +25,6 @@ Another loop (DSMR class) is setup which reads the telegram queue, stores/caches the latest telegram and notifies the Entities that the telegram has been updated. """ - import asyncio import logging from datetime import timedelta @@ -37,33 +36,33 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity -DOMAIN = 'dsmr' +_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['dsmr_parser==0.4'] +CONF_DSMR_VERSION = 'dsmr_version' + +DEFAULT_DSMR_VERSION = '2.2' +DEFAULT_PORT = '/dev/ttyUSB0' +DOMAIN = 'dsmr' + +ICON_GAS = 'mdi:fire' +ICON_POWER = 'mdi:flash' + # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -CONF_DSMR_VERSION = 'dsmr_version' -DEFAULT_PORT = '/dev/ttyUSB0' -DEFAULT_DSMR_VERSION = '2.2' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( cv.string, vol.In(['4', '2.2'])), }) -_LOGGER = logging.getLogger(__name__) - -ICON_POWER = 'mdi:flash' -ICON_GAS = 'mdi:fire' - @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup DSMR sensors.""" - # suppres logging + """Set up the DSMR sensor.""" + # Suppress logging logging.getLogger('dsmr_parser').setLevel(logging.ERROR) from dsmr_parser import obis_references as obis @@ -71,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dsmr_version = config[CONF_DSMR_VERSION] - # define list of name,obis mappings to generate entities + # Define list of name,obis mappings to generate entities obis_mapping = [ ['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE], ['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY], @@ -81,31 +80,30 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): ['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1], ['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2], ] - # protocol version specific obis + # Protocol version specific obis if dsmr_version == '4': obis_mapping.append(['Gas Consumption', obis.HOURLY_GAS_METER_READING]) else: obis_mapping.append(['Gas Consumption', obis.GAS_METER_READING]) - # generate device entities + # Generate device entities devices = [DSMREntity(name, obis) for name, obis in obis_mapping] - # setup devices yield from async_add_devices(devices) def update_entities_telegram(telegram): """Update entities with latests telegram & trigger state update.""" - # make all device entities aware of new telegram + # Make all device entities aware of new telegram for device in devices: device.telegram = telegram hass.async_add_job(device.async_update_ha_state) - # creates a asyncio.Protocol for reading DSMR telegrams from serial + # Creates a asyncio.Protocol for reading DSMR telegrams from serial # and calls update_entities_telegram to update entities on arrival dsmr = create_dsmr_reader(config[CONF_PORT], config[CONF_DSMR_VERSION], update_entities_telegram, loop=hass.loop) - # start DSMR asycnio.Protocol reader + # Start DSMR asycnio.Protocol reader transport, _ = yield from hass.loop.create_task(dsmr) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, transport.close) @@ -116,15 +114,13 @@ class DSMREntity(Entity): def __init__(self, name, obis): """"Initialize entity.""" - # human readable name self._name = name - # DSMR spec. value identifier self._obis = obis self.telegram = {} def get_dsmr_object_attr(self, attribute): """Read attribute from last received telegram for this DSMR object.""" - # make sure telegram contains an object for this entities obis + # Make sure telegram contains an object for this entities obis if self._obis not in self.telegram: return None @@ -165,9 +161,8 @@ class DSMREntity(Entity): @staticmethod def translate_tariff(value): """Convert 2/1 to normal/low.""" - # DSMR V2.2: Note: Tariff code 1 is used for low tariff - # and tariff code 2 is used for normal tariff. - + # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is + # used for normal rate. if value == '0002': return 'normal' elif value == '0001': From f1d11e77ed7eb26e5de88599bdc1ddcabbd6037b Mon Sep 17 00:00:00 2001 From: Marcel030nl Date: Thu, 24 Nov 2016 09:58:38 +0100 Subject: [PATCH 048/137] Update pvoutput.py (#4557) This addition could be usefull when working with the template sensor using the data of this sensor. --- homeassistant/components/sensor/pvoutput.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/pvoutput.py b/homeassistant/components/sensor/pvoutput.py index e0581926a32..7ee7cbe3969 100644 --- a/homeassistant/components/sensor/pvoutput.py +++ b/homeassistant/components/sensor/pvoutput.py @@ -98,6 +98,7 @@ class PvoutputSensor(Entity): """Return the state attributes of the Pi-Hole.""" if self.pvcoutput is not None: return { + ATTR_ENERGY_GENERATION: self.pvcoutput.energy_generation, ATTR_POWER_GENERATION: self.pvcoutput.power_generation, ATTR_ENERGY_CONSUMPTION: self.pvcoutput.energy_consumption, ATTR_POWER_CONSUMPTION: self.pvcoutput.power_consumption, From 14d1494cd2ade9ab12daae7546ffa189f5c68655 Mon Sep 17 00:00:00 2001 From: Matt N Date: Thu, 24 Nov 2016 01:14:38 -0800 Subject: [PATCH 049/137] systemmonitor: Support monitoring removable network interfaces (#4462) --- .../components/sensor/systemmonitor.py | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 0963786b7d0..14eadb48984 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_RESOURCES, STATE_OFF, STATE_ON, CONF_TYPE) + CONF_RESOURCES, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_TYPE) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -49,6 +49,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ })]) }) +IO_COUNTER = { + 'network_out': 0, + 'network_in': 1, + 'packets_out': 2, + 'packets_in': 3, +} + +IF_ADDRS = { + 'ipv4_address': 0, + 'ipv6_address': 1, +} + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -126,20 +138,25 @@ class SystemMonitorSensor(Entity): self._state = STATE_ON else: self._state = STATE_OFF - elif self.type == 'network_out': - self._state = round(psutil.net_io_counters(pernic=True) - [self.argument][0] / 1024**2, 1) - elif self.type == 'network_in': - self._state = round(psutil.net_io_counters(pernic=True) - [self.argument][1] / 1024**2, 1) - elif self.type == 'packets_out': - self._state = psutil.net_io_counters(pernic=True)[self.argument][2] - elif self.type == 'packets_in': - self._state = psutil.net_io_counters(pernic=True)[self.argument][3] - elif self.type == 'ipv4_address': - self._state = psutil.net_if_addrs()[self.argument][0][1] - elif self.type == 'ipv6_address': - self._state = psutil.net_if_addrs()[self.argument][1][1] + elif self.type == 'network_out' or self.type == 'network_in': + counters = psutil.net_io_counters(pernic=True) + if self.argument in counters: + counter = counters[self.argument][IO_COUNTER[self.type]] + self._state = round(counter / 1024**2, 1) + else: + self._state = STATE_UNKNOWN + elif self.type == 'packets_out' or self.type == 'packets_in': + counters = psutil.net_io_counters(pernic=True) + if self.argument in counters: + self._state = counters[self.argument][IO_COUNTER[self.type]] + else: + self._state = STATE_UNKNOWN + elif self.type == 'ipv4_address' or self.type == 'ipv6_address': + addresses = psutil.net_if_addrs() + if self.argument in addresses: + self._state = addresses[self.argument][IF_ADDRS[self.type]][1] + else: + self._state = STATE_UNKNOWN elif self.type == 'last_boot': self._state = dt_util.as_local( dt_util.utc_from_timestamp(psutil.boot_time()) From 345008c6736669b475af081427f080b0683bf540 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 24 Nov 2016 10:15:00 +0100 Subject: [PATCH 050/137] Fix docstring (#4564) --- homeassistant/components/sensor/pvoutput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/pvoutput.py b/homeassistant/components/sensor/pvoutput.py index 7ee7cbe3969..251c117f191 100644 --- a/homeassistant/components/sensor/pvoutput.py +++ b/homeassistant/components/sensor/pvoutput.py @@ -95,7 +95,7 @@ class PvoutputSensor(Entity): @property def device_state_attributes(self): - """Return the state attributes of the Pi-Hole.""" + """Return the state attributes of the monitored installation.""" if self.pvcoutput is not None: return { ATTR_ENERGY_GENERATION: self.pvcoutput.energy_generation, From 84040892df7ca5540d82dc41c4d095a682e25621 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 24 Nov 2016 12:25:01 +0100 Subject: [PATCH 051/137] Remove globally disable pylint issue (#4565) --- homeassistant/components/sensor/glances.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index cadafb8e784..0896e99989f 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -10,12 +10,12 @@ from datetime import timedelta import requests import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PORT, STATE_UNKNOWN, CONF_NAME, CONF_RESOURCES) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = 'api/2/all' @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Glances sensor.""" + """Set up the Glances sensor.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -104,7 +104,6 @@ class GlancesSensor(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement - # pylint: disable=too-many-return-statements @property def state(self): """Return the state of the resources.""" From 2a6c0cfc172eefe0c742d7adf25f8bc4bb811390 Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Thu, 24 Nov 2016 09:52:15 -0800 Subject: [PATCH 052/137] LiteJet: Unit tests and new trigger options held_more_than and held_less_than. (#4473) * LiteJet: Unit tests and new trigger options held_more_than and held_less_than. * Unit tests for the LiteJet component and associated platforms. Coverage is almost 100% -- just misses one line. * The automation LiteJet trigger returns an empty "removal" function to ensure the automation base is happy with it. The pylitejet library doesn't actually support a real removal. * The automation LiteJet trigger can detect hold time and act appropriately to support things like short tap or long hold. * LiteJet: Fix indent in unit test source code. * LiteJet: Fix test_include_switches_* unit tests on Python 3.5 * LiteJet: Remove wait for state existence from unit tests. Recent fixes to discovery make this no longer necessary. --- .coveragerc | 3 - .../components/automation/litejet.py | 63 ++++- tests/components/automation/test_litejet.py | 251 ++++++++++++++++++ tests/components/light/test_litejet.py | 168 ++++++++++++ tests/components/scene/test_litejet.py | 64 +++++ tests/components/switch/test_litejet.py | 142 ++++++++++ tests/components/test_litejet.py | 42 +++ 7 files changed, 727 insertions(+), 6 deletions(-) create mode 100644 tests/components/automation/test_litejet.py create mode 100644 tests/components/light/test_litejet.py create mode 100644 tests/components/scene/test_litejet.py create mode 100644 tests/components/switch/test_litejet.py create mode 100644 tests/components/test_litejet.py diff --git a/.coveragerc b/.coveragerc index 7b7b063edb0..b34811a6173 100644 --- a/.coveragerc +++ b/.coveragerc @@ -40,9 +40,6 @@ omit = homeassistant/components/isy994.py homeassistant/components/*/isy994.py - homeassistant/components/litejet.py - homeassistant/components/*/litejet.py - homeassistant/components/modbus.py homeassistant/components/*/modbus.py diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 875a24540ee..2b298d4979b 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -11,22 +11,34 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_point_in_utc_time DEPENDENCIES = ['litejet'] _LOGGER = logging.getLogger(__name__) CONF_NUMBER = 'number' +CONF_HELD_MORE_THAN = 'held_more_than' +CONF_HELD_LESS_THAN = 'held_less_than' TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'litejet', - vol.Required(CONF_NUMBER): cv.positive_int + vol.Required(CONF_NUMBER): cv.positive_int, + vol.Optional(CONF_HELD_MORE_THAN): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_HELD_LESS_THAN): + vol.All(cv.time_period, cv.positive_timedelta) }) def async_trigger(hass, config, action): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) + held_more_than = config.get(CONF_HELD_MORE_THAN) + held_less_than = config.get(CONF_HELD_LESS_THAN) + pressed_time = None + cancel_pressed_more_than = None @callback def call_action(): @@ -34,8 +46,53 @@ def async_trigger(hass, config, action): hass.async_run_job(action, { 'trigger': { CONF_PLATFORM: 'litejet', - CONF_NUMBER: number + CONF_NUMBER: number, + CONF_HELD_MORE_THAN: held_more_than, + CONF_HELD_LESS_THAN: held_less_than }, }) - hass.data['litejet_system'].on_switch_released(number, call_action) + # held_more_than and held_less_than: trigger on released (if in time range) + # held_more_than: trigger after pressed with calculation + # held_less_than: trigger on released with calculation + # neither: trigger on pressed + + @callback + def pressed_more_than_satisfied(now): + """Handle the LiteJet's switch's button pressed >= held_more_than.""" + call_action() + + def pressed(): + """Handle the press of the LiteJet switch's button.""" + nonlocal cancel_pressed_more_than, pressed_time + nonlocal held_less_than, held_more_than + pressed_time = dt_util.utcnow() + if held_more_than is None and held_less_than is None: + call_action() + if held_more_than is not None and held_less_than is None: + cancel_pressed_more_than = track_point_in_utc_time( + hass, + pressed_more_than_satisfied, + dt_util.utcnow() + held_more_than) + + def released(): + """Handle the release of the LiteJet switch's button.""" + nonlocal cancel_pressed_more_than, pressed_time + nonlocal held_less_than, held_more_than + # pylint: disable=not-callable + if cancel_pressed_more_than is not None: + cancel_pressed_more_than() + cancel_pressed_more_than = None + held_time = dt_util.utcnow() - pressed_time + if held_less_than is not None and held_time < held_less_than: + if held_more_than is None or held_time > held_more_than: + call_action() + + hass.data['litejet_system'].on_switch_pressed(number, pressed) + hass.data['litejet_system'].on_switch_released(number, released) + + def async_remove(): + """Remove all subscriptions used for this trigger.""" + return + + return async_remove diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py new file mode 100644 index 00000000000..be329487406 --- /dev/null +++ b/tests/components/automation/test_litejet.py @@ -0,0 +1,251 @@ +"""The tests for the litejet component.""" +import logging +import unittest +from unittest import mock +from datetime import timedelta + +from homeassistant import bootstrap +import homeassistant.util.dt as dt_util +from homeassistant.components import litejet +from tests.common import (fire_time_changed, get_test_home_assistant) +import homeassistant.components.automation as automation + +_LOGGER = logging.getLogger(__name__) + +ENTITY_SWITCH = 'switch.mock_switch_1' +ENTITY_SWITCH_NUMBER = 1 +ENTITY_OTHER_SWITCH = 'switch.mock_switch_2' +ENTITY_OTHER_SWITCH_NUMBER = 2 + + +class TestLiteJetTrigger(unittest.TestCase): + """Test the litejet component.""" + + @mock.patch('pylitejet.LiteJet') + def setup_method(self, method, mock_pylitejet): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.start() + + self.switch_pressed_callbacks = {} + self.switch_released_callbacks = {} + self.calls = [] + + def get_switch_name(number): + return "Mock Switch #"+str(number) + + def on_switch_pressed(number, callback): + self.switch_pressed_callbacks[number] = callback + + def on_switch_released(number, callback): + self.switch_released_callbacks[number] = callback + + def record_call(service): + self.calls.append(service) + + self.mock_lj = mock_pylitejet.return_value + self.mock_lj.loads.return_value = range(0) + self.mock_lj.button_switches.return_value = range(1, 3) + self.mock_lj.all_switches.return_value = range(1, 6) + self.mock_lj.scenes.return_value = range(0) + self.mock_lj.get_switch_name.side_effect = get_switch_name + self.mock_lj.on_switch_pressed.side_effect = on_switch_pressed + self.mock_lj.on_switch_released.side_effect = on_switch_released + + config = { + 'litejet': { + 'port': '/tmp/this_will_be_mocked' + } + } + assert bootstrap.setup_component(self.hass, litejet.DOMAIN, config) + + self.hass.services.register('test', 'automation', record_call) + + self.hass.block_till_done() + + self.start_time = dt_util.utcnow() + self.last_delta = timedelta(0) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def simulate_press(self, number): + _LOGGER.info('*** simulate press of %d', number) + callback = self.switch_pressed_callbacks.get(number) + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=self.start_time + self.last_delta): + if callback is not None: + callback() + self.hass.block_till_done() + + def simulate_release(self, number): + _LOGGER.info('*** simulate release of %d', number) + callback = self.switch_released_callbacks.get(number) + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=self.start_time + self.last_delta): + if callback is not None: + callback() + self.hass.block_till_done() + + def simulate_time(self, delta): + _LOGGER.info( + '*** simulate time change by %s: %s', + delta, + self.start_time + delta) + self.last_delta = delta + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=self.start_time + delta): + _LOGGER.info('now=%s', dt_util.utcnow()) + fire_time_changed(self.hass, self.start_time + delta) + self.hass.block_till_done() + _LOGGER.info('done with now=%s', dt_util.utcnow()) + + def setup_automation(self, trigger): + assert bootstrap.setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: [ + { + 'alias': 'My Test', + 'trigger': trigger, + 'action': { + 'service': 'test.automation' + } + } + ] + }) + self.hass.block_till_done() + + def test_simple(self): + """Test the simplest form of a LiteJet trigger.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + + assert len(self.calls) == 1 + + def test_held_more_than_short(self): + """Test a too short hold.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_more_than': { + 'milliseconds': '200' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + self.simulate_time(timedelta(seconds=0.1)) + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + + def test_held_more_than_long(self): + """Test a hold that is long enough.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_more_than': { + 'milliseconds': '200' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + self.simulate_time(timedelta(seconds=0.3)) + assert len(self.calls) == 1 + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 1 + + def test_held_less_than_short(self): + """Test a hold that is short enough.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_less_than': { + 'milliseconds': '200' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + self.simulate_time(timedelta(seconds=0.1)) + assert len(self.calls) == 0 + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 1 + + def test_held_less_than_long(self): + """Test a hold that is too long.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_less_than': { + 'milliseconds': '200' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + self.simulate_time(timedelta(seconds=0.3)) + assert len(self.calls) == 0 + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + + def test_held_in_range_short(self): + """Test an in-range trigger with a too short hold.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_more_than': { + 'milliseconds': '100' + }, + 'held_less_than': { + 'milliseconds': '300' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + self.simulate_time(timedelta(seconds=0.05)) + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + + def test_held_in_range_just_right(self): + """Test an in-range trigger with a just right hold.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_more_than': { + 'milliseconds': '100' + }, + 'held_less_than': { + 'milliseconds': '300' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + self.simulate_time(timedelta(seconds=0.2)) + assert len(self.calls) == 0 + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 1 + + def test_held_in_range_long(self): + """Test an in-range trigger with a too long hold.""" + self.setup_automation({ + 'platform': 'litejet', + 'number': ENTITY_OTHER_SWITCH_NUMBER, + 'held_more_than': { + 'milliseconds': '100' + }, + 'held_less_than': { + 'milliseconds': '300' + } + }) + + self.simulate_press(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 + self.simulate_time(timedelta(seconds=0.4)) + assert len(self.calls) == 0 + self.simulate_release(ENTITY_OTHER_SWITCH_NUMBER) + assert len(self.calls) == 0 diff --git a/tests/components/light/test_litejet.py b/tests/components/light/test_litejet.py new file mode 100644 index 00000000000..ab10752b14a --- /dev/null +++ b/tests/components/light/test_litejet.py @@ -0,0 +1,168 @@ +"""The tests for the litejet component.""" +import logging +import unittest +from unittest import mock + +from homeassistant import bootstrap +from homeassistant.components import litejet +from tests.common import get_test_home_assistant +import homeassistant.components.light as light + +_LOGGER = logging.getLogger(__name__) + +ENTITY_LIGHT = 'light.mock_load_1' +ENTITY_LIGHT_NUMBER = 1 +ENTITY_OTHER_LIGHT = 'light.mock_load_2' +ENTITY_OTHER_LIGHT_NUMBER = 2 + + +class TestLiteJetLight(unittest.TestCase): + """Test the litejet component.""" + + @mock.patch('pylitejet.LiteJet') + def setup_method(self, method, mock_pylitejet): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.start() + + self.load_activated_callbacks = {} + self.load_deactivated_callbacks = {} + + def get_load_name(number): + return "Mock Load #"+str(number) + + def on_load_activated(number, callback): + self.load_activated_callbacks[number] = callback + + def on_load_deactivated(number, callback): + self.load_deactivated_callbacks[number] = callback + + self.mock_lj = mock_pylitejet.return_value + self.mock_lj.loads.return_value = range(1, 3) + self.mock_lj.button_switches.return_value = range(0) + self.mock_lj.all_switches.return_value = range(0) + self.mock_lj.scenes.return_value = range(0) + self.mock_lj.get_load_level.return_value = 0 + self.mock_lj.get_load_name.side_effect = get_load_name + self.mock_lj.on_load_activated.side_effect = on_load_activated + self.mock_lj.on_load_deactivated.side_effect = on_load_deactivated + + assert bootstrap.setup_component( + self.hass, + litejet.DOMAIN, + { + 'litejet': { + 'port': '/tmp/this_will_be_mocked' + } + }) + self.hass.block_till_done() + + self.mock_lj.get_load_level.reset_mock() + + def light(self): + return self.hass.states.get(ENTITY_LIGHT) + + def other_light(self): + return self.hass.states.get(ENTITY_OTHER_LIGHT) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_on_brightness(self): + """Test turning the light on with brightness.""" + + assert self.light().state == 'off' + assert self.other_light().state == 'off' + + assert not light.is_on(self.hass, ENTITY_LIGHT) + + light.turn_on(self.hass, ENTITY_LIGHT, brightness=102) + self.hass.block_till_done() + self.mock_lj.activate_load_at.assert_called_with( + ENTITY_LIGHT_NUMBER, 39, 0) + + def test_on_off(self): + """Test turning the light on and off.""" + + assert self.light().state == 'off' + assert self.other_light().state == 'off' + + assert not light.is_on(self.hass, ENTITY_LIGHT) + + light.turn_on(self.hass, ENTITY_LIGHT) + self.hass.block_till_done() + self.mock_lj.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER) + + light.turn_off(self.hass, ENTITY_LIGHT) + self.hass.block_till_done() + self.mock_lj.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER) + + def test_activated_event(self): + """Test handling an event from LiteJet.""" + + self.mock_lj.get_load_level.return_value = 99 + + # Light 1 + + _LOGGER.info(self.load_activated_callbacks[ENTITY_LIGHT_NUMBER]) + self.load_activated_callbacks[ENTITY_LIGHT_NUMBER]() + self.hass.block_till_done() + + self.mock_lj.get_load_level.assert_called_once_with( + ENTITY_LIGHT_NUMBER) + + assert light.is_on(self.hass, ENTITY_LIGHT) + assert not light.is_on(self.hass, ENTITY_OTHER_LIGHT) + assert self.light().state == 'on' + assert self.other_light().state == 'off' + assert self.light().attributes.get(light.ATTR_BRIGHTNESS) == 255 + + # Light 2 + + self.mock_lj.get_load_level.return_value = 40 + + self.mock_lj.get_load_level.reset_mock() + + self.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + self.hass.block_till_done() + + self.mock_lj.get_load_level.assert_called_once_with( + ENTITY_OTHER_LIGHT_NUMBER) + + assert light.is_on(self.hass, ENTITY_OTHER_LIGHT) + assert light.is_on(self.hass, ENTITY_LIGHT) + assert self.light().state == 'on' + assert self.other_light().state == 'on' + assert int(self.other_light().attributes[light.ATTR_BRIGHTNESS]) == 103 + + def test_deactivated_event(self): + """Test handling an event from LiteJet.""" + + # Initial state is on. + + self.mock_lj.get_load_level.return_value = 99 + + self.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + self.hass.block_till_done() + + assert light.is_on(self.hass, ENTITY_OTHER_LIGHT) + + # Event indicates it is off now. + + self.mock_lj.get_load_level.reset_mock() + self.mock_lj.get_load_level.return_value = 0 + + self.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + self.hass.block_till_done() + + # (Requesting the level is not strictly needed with a deactivated + # event but the implementation happens to do it. This could be + # changed to a assert_not_called in the future.) + self.mock_lj.get_load_level.assert_called_with( + ENTITY_OTHER_LIGHT_NUMBER) + + assert not light.is_on(self.hass, ENTITY_OTHER_LIGHT) + assert not light.is_on(self.hass, ENTITY_LIGHT) + assert self.light().state == 'off' + assert self.other_light().state == 'off' diff --git a/tests/components/scene/test_litejet.py b/tests/components/scene/test_litejet.py new file mode 100644 index 00000000000..7596736a567 --- /dev/null +++ b/tests/components/scene/test_litejet.py @@ -0,0 +1,64 @@ +"""The tests for the litejet component.""" +import logging +import unittest +from unittest import mock + +from homeassistant import bootstrap +from homeassistant.components import litejet +from tests.common import get_test_home_assistant +import homeassistant.components.scene as scene + +_LOGGER = logging.getLogger(__name__) + +ENTITY_SCENE = 'scene.mock_scene_1' +ENTITY_SCENE_NUMBER = 1 +ENTITY_OTHER_SCENE = 'scene.mock_scene_2' +ENTITY_OTHER_SCENE_NUMBER = 2 + + +class TestLiteJetScene(unittest.TestCase): + """Test the litejet component.""" + + @mock.patch('pylitejet.LiteJet') + def setup_method(self, method, mock_pylitejet): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.start() + + def get_scene_name(number): + return "Mock Scene #"+str(number) + + self.mock_lj = mock_pylitejet.return_value + self.mock_lj.loads.return_value = range(0) + self.mock_lj.button_switches.return_value = range(0) + self.mock_lj.all_switches.return_value = range(0) + self.mock_lj.scenes.return_value = range(1, 3) + self.mock_lj.get_scene_name.side_effect = get_scene_name + + assert bootstrap.setup_component( + self.hass, + litejet.DOMAIN, + { + 'litejet': { + 'port': '/tmp/this_will_be_mocked' + } + }) + self.hass.block_till_done() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def scene(self): + return self.hass.states.get(ENTITY_SCENE) + + def other_scene(self): + return self.hass.states.get(ENTITY_OTHER_SCENE) + + def test_activate(self): + """Test activating the scene.""" + + scene.activate(self.hass, ENTITY_SCENE) + self.hass.block_till_done() + self.mock_lj.activate_scene.assert_called_once_with( + ENTITY_SCENE_NUMBER) diff --git a/tests/components/switch/test_litejet.py b/tests/components/switch/test_litejet.py new file mode 100644 index 00000000000..55d468bccd4 --- /dev/null +++ b/tests/components/switch/test_litejet.py @@ -0,0 +1,142 @@ +"""The tests for the litejet component.""" +import logging +import unittest +from unittest import mock + +from homeassistant import bootstrap +from homeassistant.components import litejet +from tests.common import get_test_home_assistant +import homeassistant.components.switch as switch + +_LOGGER = logging.getLogger(__name__) + +ENTITY_SWITCH = 'switch.mock_switch_1' +ENTITY_SWITCH_NUMBER = 1 +ENTITY_OTHER_SWITCH = 'switch.mock_switch_2' +ENTITY_OTHER_SWITCH_NUMBER = 2 + + +class TestLiteJetSwitch(unittest.TestCase): + """Test the litejet component.""" + + @mock.patch('pylitejet.LiteJet') + def setup_method(self, method, mock_pylitejet): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.start() + + self.switch_pressed_callbacks = {} + self.switch_released_callbacks = {} + + def get_switch_name(number): + return "Mock Switch #"+str(number) + + def on_switch_pressed(number, callback): + self.switch_pressed_callbacks[number] = callback + + def on_switch_released(number, callback): + self.switch_released_callbacks[number] = callback + + self.mock_lj = mock_pylitejet.return_value + self.mock_lj.loads.return_value = range(0) + self.mock_lj.button_switches.return_value = range(1, 3) + self.mock_lj.all_switches.return_value = range(1, 6) + self.mock_lj.scenes.return_value = range(0) + self.mock_lj.get_switch_name.side_effect = get_switch_name + self.mock_lj.on_switch_pressed.side_effect = on_switch_pressed + self.mock_lj.on_switch_released.side_effect = on_switch_released + + config = { + 'litejet': { + 'port': '/tmp/this_will_be_mocked', + } + } + if method == self.test_include_switches_False: + config['litejet']['include_switches'] = False + elif method != self.test_include_switches_unspecified: + config['litejet']['include_switches'] = True + + assert bootstrap.setup_component(self.hass, litejet.DOMAIN, config) + self.hass.block_till_done() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def switch(self): + return self.hass.states.get(ENTITY_SWITCH) + + def other_switch(self): + return self.hass.states.get(ENTITY_OTHER_SWITCH) + + def test_include_switches_unspecified(self): + """Test that switches are ignored by default.""" + + self.mock_lj.button_switches.assert_not_called() + self.mock_lj.all_switches.assert_not_called() + + def test_include_switches_False(self): + """Test that switches can be explicitly ignored.""" + + self.mock_lj.button_switches.assert_not_called() + self.mock_lj.all_switches.assert_not_called() + + def test_on_off(self): + """Test turning the switch on and off.""" + + assert self.switch().state == 'off' + assert self.other_switch().state == 'off' + + assert not switch.is_on(self.hass, ENTITY_SWITCH) + + switch.turn_on(self.hass, ENTITY_SWITCH) + self.hass.block_till_done() + self.mock_lj.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER) + + switch.turn_off(self.hass, ENTITY_SWITCH) + self.hass.block_till_done() + self.mock_lj.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER) + + def test_pressed_event(self): + """Test handling an event from LiteJet.""" + + # Switch 1 + + _LOGGER.info(self.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]) + self.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]() + self.hass.block_till_done() + + assert switch.is_on(self.hass, ENTITY_SWITCH) + assert not switch.is_on(self.hass, ENTITY_OTHER_SWITCH) + assert self.switch().state == 'on' + assert self.other_switch().state == 'off' + + # Switch 2 + + self.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() + self.hass.block_till_done() + + assert switch.is_on(self.hass, ENTITY_OTHER_SWITCH) + assert switch.is_on(self.hass, ENTITY_SWITCH) + assert self.other_switch().state == 'on' + assert self.switch().state == 'on' + + def test_released_event(self): + """Test handling an event from LiteJet.""" + + # Initial state is on. + + self.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() + self.hass.block_till_done() + + assert switch.is_on(self.hass, ENTITY_OTHER_SWITCH) + + # Event indicates it is off now. + + self.switch_released_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() + self.hass.block_till_done() + + assert not switch.is_on(self.hass, ENTITY_OTHER_SWITCH) + assert not switch.is_on(self.hass, ENTITY_SWITCH) + assert self.other_switch().state == 'off' + assert self.switch().state == 'off' diff --git a/tests/components/test_litejet.py b/tests/components/test_litejet.py new file mode 100644 index 00000000000..6d62e1ab0cd --- /dev/null +++ b/tests/components/test_litejet.py @@ -0,0 +1,42 @@ +"""The tests for the litejet component.""" +import logging +import unittest + +from homeassistant.components import litejet +from tests.common import get_test_home_assistant + +_LOGGER = logging.getLogger(__name__) + + +class TestLiteJet(unittest.TestCase): + """Test the litejet component.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.start() + self.hass.block_till_done() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_is_ignored_unspecified(self): + self.hass.data['litejet_config'] = {} + assert not litejet.is_ignored(self.hass, 'Test') + + def test_is_ignored_empty(self): + self.hass.data['litejet_config'] = { + litejet.CONF_EXCLUDE_NAMES: [] + } + assert not litejet.is_ignored(self.hass, 'Test') + + def test_is_ignored_normal(self): + self.hass.data['litejet_config'] = { + litejet.CONF_EXCLUDE_NAMES: ['Test', 'Other One'] + } + assert litejet.is_ignored(self.hass, 'Test') + assert not litejet.is_ignored(self.hass, 'Other one') + assert not litejet.is_ignored(self.hass, 'Other 0ne') + assert litejet.is_ignored(self.hass, 'Other One There') + assert litejet.is_ignored(self.hass, 'Other One') From 42c99b0ccb650fcf64cb16d8427e25296f00f047 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Nov 2016 14:02:39 -0800 Subject: [PATCH 053/137] Pass hass object to ServiceRegistry constructor (#4570) --- homeassistant/core.py | 35 ++++++++++++++++------------------- homeassistant/remote.py | 2 +- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 50c805e2548..645bdc68b0a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -115,8 +115,7 @@ class HomeAssistant(object): self._pending_tasks = [] self._pending_sheduler = None self.bus = EventBus(self) - self.services = ServiceRegistry(self.bus, self.async_add_job, - self.loop) + self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) self.config = Config() # type: Config # This is a dictionary that any component can store any data on. @@ -849,12 +848,10 @@ class ServiceCall(object): class ServiceRegistry(object): """Offers services over the eventbus.""" - def __init__(self, bus, async_add_job, loop): + def __init__(self, hass): """Initialize a service registry.""" self._services = {} - self._async_add_job = async_add_job - self._bus = bus - self._loop = loop + self._hass = hass self._cur_id = 0 self._async_unsub_call_event = None @@ -862,7 +859,7 @@ class ServiceRegistry(object): def services(self): """Dict with per domain a list of available services.""" return run_callback_threadsafe( - self._loop, self.async_services, + self._hass.loop, self.async_services, ).result() @callback @@ -893,7 +890,7 @@ class ServiceRegistry(object): Schema is called to coerce and validate the service data. """ run_callback_threadsafe( - self._loop, + self._hass.loop, self.async_register, domain, service, service_func, description, schema ).result() @@ -923,10 +920,10 @@ class ServiceRegistry(object): self._services[domain] = {service: service_obj} if self._async_unsub_call_event is None: - self._async_unsub_call_event = self._bus.async_listen( + self._async_unsub_call_event = self._hass.bus.async_listen( EVENT_CALL_SERVICE, self._event_to_service_call) - self._bus.async_fire( + self._hass.bus.async_fire( EVENT_SERVICE_REGISTERED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) @@ -950,7 +947,7 @@ class ServiceRegistry(object): """ return run_coroutine_threadsafe( self.async_call(domain, service, service_data, blocking), - self._loop + self._hass.loop ).result() @asyncio.coroutine @@ -983,7 +980,7 @@ class ServiceRegistry(object): } if blocking: - fut = asyncio.Future(loop=self._loop) + fut = asyncio.Future(loop=self._hass.loop) @callback def service_executed(event): @@ -991,13 +988,13 @@ class ServiceRegistry(object): if event.data[ATTR_SERVICE_CALL_ID] == call_id: fut.set_result(True) - unsub = self._bus.async_listen(EVENT_SERVICE_EXECUTED, - service_executed) + unsub = self._hass.bus.async_listen(EVENT_SERVICE_EXECUTED, + service_executed) - self._bus.async_fire(EVENT_CALL_SERVICE, event_data) + self._hass.bus.async_fire(EVENT_CALL_SERVICE, event_data) if blocking: - done, _ = yield from asyncio.wait([fut], loop=self._loop, + done, _ = yield from asyncio.wait([fut], loop=self._hass.loop, timeout=SERVICE_CALL_LIMIT) success = bool(done) unsub() @@ -1028,9 +1025,9 @@ class ServiceRegistry(object): if (service_handler.is_coroutinefunction or service_handler.is_callback): - self._bus.async_fire(EVENT_SERVICE_EXECUTED, data) + self._hass.bus.async_fire(EVENT_SERVICE_EXECUTED, data) else: - self._bus.fire(EVENT_SERVICE_EXECUTED, data) + self._hass.bus.fire(EVENT_SERVICE_EXECUTED, data) try: if service_handler.schema: @@ -1055,7 +1052,7 @@ class ServiceRegistry(object): service_handler.func(service_call) fire_service_executed() - self._async_add_job(execute_service) + self._hass.async_add_job(execute_service) def _generate_unique_id(self): """Generate a unique service call id.""" diff --git a/homeassistant/remote.py b/homeassistant/remote.py index c4293680ec5..fa6cb446c67 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -131,7 +131,7 @@ class HomeAssistant(ha.HomeAssistant): self._pending_sheduler = None self.bus = EventBus(remote_api, self) - self.services = ha.ServiceRegistry(self.bus, self.add_job, self.loop) + self.services = ha.ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop, self.remote_api) self.config = ha.Config() # This is a dictionary that any component can store any data on. From eacdce9ed99cf74f0b8d8ccb8f12233d1b2c4ceb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Nov 2016 14:49:29 -0800 Subject: [PATCH 054/137] Track tasks only during shutdown and tests (#4428) * Track tasks only when needed * Tweak async_block_till_done --- homeassistant/core.py | 69 +++++++++++++++++++++---------------------- tests/common.py | 6 ++-- tests/test_core.py | 14 +++------ tests/test_remote.py | 1 + 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 645bdc68b0a..de798434956 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -113,7 +113,6 @@ class HomeAssistant(object): self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) self._pending_tasks = [] - self._pending_sheduler = None self.bus = EventBus(self) self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) @@ -185,24 +184,10 @@ class HomeAssistant(object): # pylint: disable=protected-access self.loop._thread_ident = threading.get_ident() - self._async_tasks_cleanup() _async_create_timer(self) self.bus.async_fire(EVENT_HOMEASSISTANT_START) self.state = CoreState.running - @callback - def _async_tasks_cleanup(self): - """Cleanup all pending tasks in a time interval. - - This method must be run in the event loop. - """ - self._pending_tasks = [task for task in self._pending_tasks - if not task.done()] - - # sheduled next cleanup - self._pending_sheduler = self.loop.call_later( - TIME_INTERVAL_TASKS_CLEANUP, self._async_tasks_cleanup) - def add_job(self, target: Callable[..., None], *args: Any) -> None: """Add job to the executor pool. @@ -212,7 +197,28 @@ class HomeAssistant(object): self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback - def async_add_job(self, target: Callable[..., None], *args: Any) -> None: + def _async_add_job(self, target: Callable[..., None], *args: Any) -> None: + """Add a job from within the eventloop. + + This method must be run in the event loop. + + target: target to call. + args: parameters for method to call. + """ + if asyncio.iscoroutine(target): + self.loop.create_task(target) + elif is_callback(target): + self.loop.call_soon(target, *args) + elif asyncio.iscoroutinefunction(target): + self.loop.create_task(target(*args)) + else: + self.loop.run_in_executor(None, target, *args) + + async_add_job = _async_add_job + + @callback + def _async_add_job_tracking(self, target: Callable[..., None], + *args: Any) -> None: """Add a job from within the eventloop. This method must be run in the event loop. @@ -235,6 +241,11 @@ class HomeAssistant(object): if task is not None: self._pending_tasks.append(task) + @callback + def async_track_tasks(self): + """Track tasks so you can wait for all tasks to be done.""" + self.async_add_job = self._async_add_job_tracking + @callback def async_run_job(self, target: Callable[..., None], *args: Any) -> None: """Run a job from within the event loop. @@ -249,16 +260,6 @@ class HomeAssistant(object): else: self.async_add_job(target, *args) - def _loop_empty(self) -> bool: - """Python 3.4.2 empty loop compatibility function.""" - # pylint: disable=protected-access - if sys.version_info < (3, 4, 3): - return len(self.loop._scheduled) == 0 and \ - len(self.loop._ready) == 0 - else: - return self.loop._current_handle is None and \ - len(self.loop._ready) == 0 - def block_till_done(self) -> None: """Block till all pending work is done.""" run_coroutine_threadsafe( @@ -267,18 +268,17 @@ class HomeAssistant(object): @asyncio.coroutine def async_block_till_done(self): """Block till all pending work is done.""" - while True: - # Wait for the pending tasks are down + # To flush out any call_soon_threadsafe + yield from asyncio.sleep(0, loop=self.loop) + + while self._pending_tasks: pending = [task for task in self._pending_tasks if not task.done()] self._pending_tasks.clear() if len(pending) > 0: yield from asyncio.wait(pending, loop=self.loop) - - # Verify the loop is empty - ret = yield from self.loop.run_in_executor(None, self._loop_empty) - if ret and not self._pending_tasks: - break + else: + yield from asyncio.sleep(0, loop=self.loop) def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" @@ -291,9 +291,8 @@ class HomeAssistant(object): This method is a coroutine. """ self.state = CoreState.stopping + self.async_track_tasks() self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - if self._pending_sheduler is not None: - self._pending_sheduler.cancel() yield from self.async_block_till_done() self.executor.shutdown() if self._websession is not None: diff --git a/tests/common.py b/tests/common.py index 525d7f85bd3..25a10783c28 100644 --- a/tests/common.py +++ b/tests/common.py @@ -82,6 +82,7 @@ def async_test_home_assistant(loop): loop._thread_ident = threading.get_ident() hass = ha.HomeAssistant(loop) + hass.async_track_tasks() hass.config.location_name = 'test home' hass.config.config_dir = get_test_config_dir() @@ -103,9 +104,8 @@ def async_test_home_assistant(loop): @asyncio.coroutine def mock_async_start(): """Start the mocking.""" - with patch.object(loop, 'add_signal_handler'),\ - patch('homeassistant.core._async_create_timer'),\ - patch.object(hass, '_async_tasks_cleanup', return_value=None): + with patch.object(loop, 'add_signal_handler'), \ + patch('homeassistant.core._async_create_timer'): yield from orig_start() hass.async_start = mock_async_start diff --git a/tests/test_core.py b/tests/test_core.py index 212c6d41f70..9221ad68352 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,8 +9,7 @@ import pytz import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError -from homeassistant.util.async import ( - run_callback_threadsafe, run_coroutine_threadsafe) +from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( @@ -129,7 +128,7 @@ class TestHomeAssistant(unittest.TestCase): """Test Coro.""" call_count.append('call') - for i in range(50): + for i in range(3): self.hass.add_job(test_coro()) run_coroutine_threadsafe( @@ -137,13 +136,8 @@ class TestHomeAssistant(unittest.TestCase): loop=self.hass.loop ).result() - with patch.object(self.hass.loop, 'call_later') as mock_later: - run_callback_threadsafe( - self.hass.loop, self.hass._async_tasks_cleanup).result() - assert mock_later.called - - assert len(self.hass._pending_tasks) == 0 - assert len(call_count) == 50 + assert len(self.hass._pending_tasks) == 3 + assert len(call_count) == 3 def test_async_add_job_pending_tasks_coro(self): """Add a coro to pending tasks.""" diff --git a/tests/test_remote.py b/tests/test_remote.py index 55d8ca18b5f..fa2a53a96cb 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -61,6 +61,7 @@ def setUpModule(): target=loop.run_forever).start() slave = remote.HomeAssistant(master_api, loop=loop) + slave.async_track_tasks() slave.config.config_dir = get_test_config_dir() slave.config.skip_pip = True bootstrap.setup_component( From febe16d70084516856b2f1b16227e85c12f488da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Nov 2016 14:56:33 -0800 Subject: [PATCH 055/137] Set executor pool size to 10 (#4571) --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index de798434956..42ab117eadc 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -56,7 +56,7 @@ SERVICE_CALL_LIMIT = 10 # seconds ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") # Size of a executor pool -EXECUTOR_POOL_SIZE = 15 +EXECUTOR_POOL_SIZE = 10 # Time for cleanup internal pending tasks TIME_INTERVAL_TASKS_CLEANUP = 10 From 95b439fbd5ad96966dfe1176697c6ee4731a6207 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Fri, 25 Nov 2016 05:37:56 +0000 Subject: [PATCH 056/137] Upgrade aiohttp to 1.1.5 (#4213) --- homeassistant/components/frontend/__init__.py | 70 ++++++++++++------- homeassistant/components/http.py | 58 ++++++++------- requirements_all.txt | 6 +- requirements_test.txt | 2 +- setup.py | 4 +- tests/components/media_player/test_demo.py | 10 +-- tests/components/test_google.py | 36 +++++----- tests/components/test_panel_iframe.py | 10 ++- 8 files changed, 106 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 195d79ec5da..6fde1ae388a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -8,7 +8,7 @@ import os from aiohttp import web from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND +from homeassistant.const import HTTP_NOT_FOUND from homeassistant.components import api, group from homeassistant.components.http import HomeAssistantView from .version import FINGERPRINTS @@ -18,7 +18,6 @@ DEPENDENCIES = ['api'] URL_PANEL_COMPONENT = '/frontend/panels/{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') -PANELS = {} MANIFEST_JSON = { "background_color": "#FFFFFF", "description": "Open-source home automation platform running on Python 3.", @@ -32,6 +31,16 @@ MANIFEST_JSON = { "theme_color": "#03A9F4" } +for size in (192, 384, 512, 1024): + MANIFEST_JSON['icons'].append({ + "src": "/static/icons/favicon-{}x{}.png".format(size, size), + "sizes": "{}x{}".format(size, size), + "type": "image/png" + }) + +DATA_PANELS = 'frontend_panels' +DATA_INDEX_VIEW = 'frontend_index_view' + # To keep track we don't register a component twice (gives a warning) _REGISTERED_COMPONENTS = set() _LOGGER = logging.getLogger(__name__) @@ -68,10 +77,14 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, Warning: this API will probably change. Use at own risk. """ + panels = hass.data.get(DATA_PANELS) + if panels is None: + panels = hass.data[DATA_PANELS] = {} + if url_path is None: url_path = component_name - if url_path in PANELS: + if url_path in panels: _LOGGER.warning('Overwriting component %s', url_path) if not os.path.isfile(path): _LOGGER.error('Panel %s component does not exist: %s', @@ -106,7 +119,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) data['url'] = fprinted_url - PANELS[url_path] = data + panels[url_path] = data + + # Register index view for this route if IndexView already loaded + # Otherwise it will be done during setup. + index_view = hass.data.get(DATA_INDEX_VIEW) + + if index_view: + hass.http.app.router.add_route('get', '/{}'.format(url_path), + index_view.get) def add_manifest_json_key(key, val): @@ -134,29 +155,24 @@ def setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local) + index_view = hass.data[DATA_INDEX_VIEW] = IndexView(hass) + hass.http.register_view(index_view) + + # Components have registered panels before frontend got setup. + # Now register their urls. + if DATA_PANELS in hass.data: + for url_path in hass.data[DATA_PANELS]: + hass.http.app.router.add_route('get', '/{}'.format(url_path), + index_view.get) + else: + hass.data[DATA_PANELS] = {} + register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', 'dev-template'): register_built_in_panel(hass, panel) - def register_frontend_index(event): - """Register the frontend index urls. - - Done when Home Assistant is started so that all panels are known. - """ - hass.http.register_view(IndexView( - hass, ['/{}'.format(name) for name in PANELS])) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) - - for size in (192, 384, 512, 1024): - MANIFEST_JSON['icons'].append({ - "src": "/static/icons/favicon-{}x{}.png".format(size, size), - "sizes": "{}x{}".format(size, size), - "type": "image/png" - }) - return True @@ -174,7 +190,7 @@ class BootstrapView(HomeAssistantView): 'states': self.hass.states.async_all(), 'events': api.async_events_json(self.hass), 'services': api.async_services_json(self.hass), - 'panels': PANELS, + 'panels': self.hass.data[DATA_PANELS], }) @@ -186,13 +202,12 @@ class IndexView(HomeAssistantView): requires_auth = False extra_urls = ['/states', '/states/{entity_id}'] - def __init__(self, hass, extra_urls): + def __init__(self, hass): """Initialize the frontend view.""" super().__init__(hass) from jinja2 import FileSystemLoader, Environment - self.extra_urls = self.extra_urls + extra_urls self.templates = Environment( loader=FileSystemLoader( os.path.join(os.path.dirname(__file__), 'templates/') @@ -223,7 +238,10 @@ class IndexView(HomeAssistantView): else: panel = request.path.split('/')[1] - panel_url = PANELS[panel]['url'] if panel != 'states' else '' + if panel == 'states': + panel_url = '' + else: + panel_url = self.hass.data[DATA_PANELS][panel]['url'] no_auth = 'true' if self.hass.config.api.api_password: @@ -244,7 +262,7 @@ class IndexView(HomeAssistantView): resp = template.render( core_url=core_url, ui_url=ui_url, no_auth=no_auth, icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], - panel_url=panel_url, panels=PANELS) + panel_url=panel_url, panels=self.hass.data[DATA_PANELS]) return web.Response(text=resp, content_type='text/html') diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index a008a3f4db6..a6293d07e6e 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -20,7 +20,7 @@ from aiohttp import web, hdrs from aiohttp.file_sender import FileSender from aiohttp.web_exceptions import ( HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified) -from aiohttp.web_urldispatcher import StaticRoute +from aiohttp.web_urldispatcher import StaticResource from homeassistant.core import is_callback import homeassistant.remote as rem @@ -33,7 +33,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components import persistent_notification DOMAIN = 'http' -REQUIREMENTS = ('aiohttp_cors==0.4.0',) +REQUIREMENTS = ('aiohttp_cors==0.5.0',) CONF_API_PASSWORD = 'api_password' CONF_SERVER_HOST = 'server_host' @@ -212,13 +212,8 @@ class GzipFileSender(FileSender): file_size = st.st_size resp.content_length = file_size - resp.set_tcp_cork(True) - try: - with filepath.open('rb') as f: - yield from self._sendfile(request, resp, f, file_size) - - finally: - resp.set_tcp_nodelay(True) + with filepath.open('rb') as f: + yield from self._sendfile(request, resp, f, file_size) return resp @@ -226,26 +221,32 @@ class GzipFileSender(FileSender): _GZIP_FILE_SENDER = GzipFileSender() -class HAStaticRoute(StaticRoute): - """StaticRoute with support for fingerprinting.""" +@asyncio.coroutine +def staticresource_enhancer(app, handler): + """Enhance StaticResourceHandler. - def __init__(self, prefix, path): - """Initialize a static route with gzip and cache busting support.""" - super().__init__(None, prefix, path) - self._file_sender = _GZIP_FILE_SENDER + Adds gzip encoding and fingerprinting matching. + """ + inst = getattr(handler, '__self__', None) + if not isinstance(inst, StaticResource): + return handler - def match(self, path): - """Match path to filename.""" - if not path.startswith(self._prefix): - return None + # pylint: disable=protected-access + inst._file_sender = _GZIP_FILE_SENDER + + @asyncio.coroutine + def middleware_handler(request): + """Strip out fingerprints from resource names.""" + fingerprinted = _FINGERPRINT.match(request.match_info['filename']) - # Extra sauce to remove fingerprinted resource names - filename = path[self._prefix_len:] - fingerprinted = _FINGERPRINT.match(filename) if fingerprinted: - filename = '{}.{}'.format(*fingerprinted.groups()) + request.match_info['filename'] = \ + '{}.{}'.format(*fingerprinted.groups()) - return {'filename': filename} + resp = yield from handler(request) + return resp + + return middleware_handler class HomeAssistantWSGI(object): @@ -257,7 +258,8 @@ class HomeAssistantWSGI(object): """Initialize the WSGI Home Assistant server.""" import aiohttp_cors - self.app = web.Application(loop=hass.loop) + self.app = web.Application(middlewares=[staticresource_enhancer], + loop=hass.loop) self.hass = hass self.development = development self.api_password = api_password @@ -318,11 +320,7 @@ class HomeAssistantWSGI(object): Specify optional cache length of asset in days. """ if os.path.isdir(path): - assert url_root.startswith('/') - if not url_root.endswith('/'): - url_root += '/' - route = HAStaticRoute(url_root, path) - self.app.router.register_route(route) + self.app.router.add_static(url_root, path) return filepath = Path(path) diff --git a/requirements_all.txt b/requirements_all.txt index af47a1daac9..f9ff30d8ad4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,8 +6,8 @@ pip>=7.0.0 jinja2>=2.8 voluptuous==0.9.2 typing>=3,<4 -aiohttp==1.0.5 -async_timeout==1.0.0 +aiohttp==1.1.5 +async_timeout==1.1.0 # homeassistant.components.nuimo_controller --only-binary=all git+https://github.com/getSenic/nuimo-linux-python#nuimo==1.0.0 @@ -31,7 +31,7 @@ SoCo==0.12 TwitterAPI==2.4.2 # homeassistant.components.http -aiohttp_cors==0.4.0 +aiohttp_cors==0.5.0 # homeassistant.components.apcupsd apcaccess==0.0.4 diff --git a/requirements_test.txt b/requirements_test.txt index 784631867ce..838e4c96875 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ pytest>=2.9.2 pytest-aiohttp>=0.1.3 pytest-asyncio>=0.5.0 pytest-cov>=2.3.1 -pytest-timeout>=1.0.0 +pytest-timeout>=1.2.0 pytest-catchlog>=1.2.2 requests_mock>=1.0 mock-open>=1.3.1 diff --git a/setup.py b/setup.py index 145b027e975..7060550c723 100755 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ REQUIRES = [ 'jinja2>=2.8', 'voluptuous==0.9.2', 'typing>=3,<4', - 'aiohttp==1.0.5', - 'async_timeout==1.0.0', + 'aiohttp==1.1.5', + 'async_timeout==1.1.0', ] setup( diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index 8bcda323010..3539c73b7dd 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -245,13 +245,17 @@ class TestMediaPlayerWeb(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - setup_component(self.hass, http.DOMAIN, { + assert setup_component(self.hass, http.DOMAIN, { http.DOMAIN: { http.CONF_SERVER_PORT: SERVER_PORT, http.CONF_API_PASSWORD: API_PASSWORD, }, }) + assert setup_component( + self.hass, mp.DOMAIN, + {'media_player': {'platform': 'demo'}}) + self.hass.start() def tearDown(self): @@ -287,10 +291,6 @@ class TestMediaPlayerWeb(unittest.TestCase): self.hass._websession = MockWebsession() - self.hass.block_till_done() - assert setup_component( - self.hass, mp.DOMAIN, - {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') state = self.hass.states.get(entity_id) req = requests.get(HTTP_BASE_URL + diff --git a/tests/components/test_google.py b/tests/components/test_google.py index 10db913aa81..fbaddb1ed32 100644 --- a/tests/components/test_google.py +++ b/tests/components/test_google.py @@ -34,6 +34,7 @@ class TestGoogle(unittest.TestCase): self.assertTrue(setup_component(self.hass, 'google', config)) def test_get_calendar_info(self): + """Test getting the calendar info.""" calendar = { 'id': 'qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com', 'etag': '"3584134138943410"', @@ -61,21 +62,22 @@ class TestGoogle(unittest.TestCase): }) def test_found_calendar(self): - calendar = { - 'id': 'qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com', - 'etag': '"3584134138943410"', - 'timeZone': 'UTC', - 'accessRole': 'reader', - 'foregroundColor': '#000000', - 'selected': True, - 'kind': 'calendar#calendarListEntry', - 'backgroundColor': '#16a765', - 'description': 'Test Calendar', - 'summary': 'We are, we are, a... Test Calendar', - 'colorId': '8', - 'defaultReminders': [], - 'track': True - } + """Test when a calendar is found.""" + # calendar = { + # 'id': 'qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com', + # 'etag': '"3584134138943410"', + # 'timeZone': 'UTC', + # 'accessRole': 'reader', + # 'foregroundColor': '#000000', + # 'selected': True, + # 'kind': 'calendar#calendarListEntry', + # 'backgroundColor': '#16a765', + # 'description': 'Test Calendar', + # 'summary': 'We are, we are, a... Test Calendar', + # 'colorId': '8', + # 'defaultReminders': [], + # 'track': True + # } # self.assertIsInstance(self.hass.data[google.DATA_INDEX], dict) # self.assertEquals(self.hass.data[google.DATA_INDEX], {}) @@ -84,8 +86,8 @@ class TestGoogle(unittest.TestCase): self.hass.config.path(google.TOKEN_FILE)) self.assertTrue(google.setup_services(self.hass, True, calendar_service)) - self.hass.services.call('google', 'found_calendar', calendar, - blocking=True) + # self.hass.services.call('google', 'found_calendar', calendar, + # blocking=True) # TODO: Fix this # self.assertTrue(self.hass.data[google.DATA_INDEX] diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index ac479dea645..cf2fdc23b09 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -14,12 +14,10 @@ class TestPanelIframe(unittest.TestCase): def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - frontend.PANELS = {} def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - frontend.PANELS = {} def test_wrong_config(self): """Test setup with wrong configuration.""" @@ -55,9 +53,9 @@ class TestPanelIframe(unittest.TestCase): }, }) - # 5 dev tools + map are automatically loaded - assert len(frontend.PANELS) == 8 - assert frontend.PANELS['router'] == { + # 5 dev tools + map are automatically loaded + 2 iframe panels + assert len(self.hass.data[frontend.DATA_PANELS]) == 8 + assert self.hass.data[frontend.DATA_PANELS]['router'] == { 'component_name': 'iframe', 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', @@ -66,7 +64,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'router' } - assert frontend.PANELS['weather'] == { + assert self.hass.data[frontend.DATA_PANELS]['weather'] == { 'component_name': 'iframe', 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', From 2a7bc0e55c4baa9ec8b4c2562cd28c1c86f44957 Mon Sep 17 00:00:00 2001 From: Vlad Korniev Date: Thu, 24 Nov 2016 21:52:10 -0800 Subject: [PATCH 057/137] Advanced Ip filtering (#4424) * Added IP Bans configuration * Fixing warnings * Added ban enabled option and unit tests * Fixed py34 tox * http: requested changes fix * Requested changes fix --- homeassistant/components/emulated_hue.py | 7 +- homeassistant/components/http.py | 138 +++++++++++++++++++-- homeassistant/helpers/config_validation.py | 18 ++- tests/components/notify/test_html5.py | 7 ++ tests/components/test_http.py | 60 ++++++++- tests/helpers/test_config_validation.py | 13 +- 6 files changed, 225 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index ad89e001df0..afb5c63918c 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -75,9 +75,12 @@ def setup(hass, yaml_config): api_password=None, ssl_certificate=None, ssl_key=None, - cors_origins=[], + cors_origins=None, use_x_forwarded_for=False, - trusted_networks=[] + trusted_networks=None, + ip_bans=None, + login_threshold=0, + is_ban_enabled=False ) server.register_view(DescriptionXmlView(hass, config)) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index a6293d07e6e..054d8050599 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -5,32 +5,36 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/http/ """ import asyncio -import hmac import json import logging import mimetypes -import os -from pathlib import Path -import re import ssl +from datetime import datetime from ipaddress import ip_address, ip_network +from pathlib import Path +import hmac +import os +import re import voluptuous as vol from aiohttp import web, hdrs from aiohttp.file_sender import FileSender from aiohttp.web_exceptions import ( - HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified) + HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified, HTTPForbidden) from aiohttp.web_urldispatcher import StaticResource -from homeassistant.core import is_callback +import homeassistant.helpers.config_validation as cv import homeassistant.remote as rem from homeassistant import util +from homeassistant.components import persistent_notification +from homeassistant.config import load_yaml_config_file from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR) -import homeassistant.helpers.config_validation as cv -from homeassistant.components import persistent_notification +from homeassistant.core import is_callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.yaml import dump DOMAIN = 'http' REQUIREMENTS = ('aiohttp_cors==0.5.0',) @@ -44,9 +48,16 @@ CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' CONF_TRUSTED_NETWORKS = 'trusted_networks' +CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' +CONF_IP_BAN_ENABLED = 'ip_ban_enabled' DATA_API_PASSWORD = 'api_password' NOTIFICATION_ID_LOGIN = 'http-login' +NOTIFICATION_ID_BAN = 'ip-ban' + +IP_BANS = 'ip_bans.yaml' +ATTR_BANNED_AT = "banned_at" + # TLS configuation follows the best-practice guidelines specified here: # https://wiki.mozilla.org/Security/Server_Side_TLS @@ -85,7 +96,9 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, vol.Optional(CONF_TRUSTED_NETWORKS): - vol.All(cv.ensure_list, [ip_network]) + vol.All(cv.ensure_list, [ip_network]), + vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD): cv.positive_int, + vol.Optional(CONF_IP_BAN_ENABLED): cv.boolean }), }, extra=vol.ALLOW_EXTRA) @@ -131,6 +144,9 @@ def setup(hass, config): trusted_networks = [ ip_network(trusted_network) for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])] + is_ban_enabled = bool(conf.get(CONF_IP_BAN_ENABLED, False)) + login_threshold = int(conf.get(CONF_LOGIN_ATTEMPTS_THRESHOLD, -1)) + ip_bans = load_ip_bans_config(hass.config.path(IP_BANS)) server = HomeAssistantWSGI( hass, @@ -142,7 +158,10 @@ def setup(hass, config): ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, - trusted_networks=trusted_networks + trusted_networks=trusted_networks, + ip_bans=ip_bans, + login_threshold=login_threshold, + is_ban_enabled=is_ban_enabled ) @asyncio.coroutine @@ -254,7 +273,8 @@ class HomeAssistantWSGI(object): def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_networks): + use_x_forwarded_for, trusted_networks, + ip_bans, login_threshold, is_ban_enabled): """Initialize the WSGI Home Assistant server.""" import aiohttp_cors @@ -268,10 +288,15 @@ class HomeAssistantWSGI(object): self.server_host = server_host self.server_port = server_port self.use_x_forwarded_for = use_x_forwarded_for - self.trusted_networks = trusted_networks + self.trusted_networks = trusted_networks \ + if trusted_networks is not None else [] self.event_forwarder = None self._handler = None self.server = None + self.login_threshold = login_threshold + self.ip_bans = ip_bans if ip_bans is not None else [] + self.failed_login_attempts = {} + self.is_ban_enabled = is_ban_enabled if cors_origins: self.cors = aiohttp_cors.setup(self.app, defaults={ @@ -385,6 +410,39 @@ class HomeAssistantWSGI(object): return any(ip_address(remote_addr) in trusted_network for trusted_network in self.hass.http.trusted_networks) + def wrong_login_attempt(self, remote_addr): + """Registering wrong login attempt.""" + if not self.is_ban_enabled or self.login_threshold < 1: + return + + if remote_addr in self.failed_login_attempts: + self.failed_login_attempts[remote_addr] += 1 + else: + self.failed_login_attempts[remote_addr] = 1 + + if self.failed_login_attempts[remote_addr] > self.login_threshold: + new_ban = IpBan(remote_addr) + self.ip_bans.append(new_ban) + update_ip_bans_config(self.hass.config.path(IP_BANS), new_ban) + _LOGGER.warning('Banned IP %s for too many login attempts', + remote_addr) + persistent_notification.async_create( + self.hass, + 'Too many login attempts from {}'.format(remote_addr), + 'Banning IP address', NOTIFICATION_ID_BAN) + + def is_banned_ip(self, remote_addr): + """Check if IP address is in a ban list.""" + if not self.is_ban_enabled: + return False + + ip_address_ = ip_address(remote_addr) + for ip_ban in self.ip_bans: + if ip_ban.ip_address == ip_address_: + return True + + return False + class HomeAssistantView(object): """Base view for all views.""" @@ -465,6 +523,9 @@ def request_handler_factory(view, handler): remote_addr = view.hass.http.get_real_ip(request) + if view.hass.http.is_banned_ip(remote_addr): + raise HTTPForbidden() + # Auth code verbose on purpose authenticated = False @@ -484,6 +545,7 @@ def request_handler_factory(view, handler): authenticated = True if view.requires_auth and not authenticated: + view.hass.http.wrong_login_attempt(remote_addr) _LOGGER.warning('Login attempt or request with an invalid ' 'password from %s', remote_addr) persistent_notification.async_create( @@ -525,3 +587,55 @@ def request_handler_factory(view, handler): return web.Response(body=result, status=status_code) return handle + + +class IpBan(object): + """Represents banned IP address.""" + + def __init__(self, ip_ban: str, banned_at: datetime=None) -> None: + """Initializing Ip Ban object.""" + self.ip_address = ip_address(ip_ban) + self.banned_at = banned_at + if self.banned_at is None: + self.banned_at = datetime.utcnow() + + +def load_ip_bans_config(path: str): + """Loading list of banned IPs from config file.""" + ip_list = [] + ip_schema = vol.Schema({ + vol.Optional('banned_at'): vol.Any(None, cv.datetime) + }) + + try: + try: + list_ = load_yaml_config_file(path) + except HomeAssistantError as err: + _LOGGER.error('Unable to load %s: %s', path, str(err)) + return [] + + for ip_ban, ip_info in list_.items(): + try: + ip_info = ip_schema(ip_info) + ip_info['ip_ban'] = ip_address(ip_ban) + ip_list.append(IpBan(**ip_info)) + except vol.Invalid: + _LOGGER.exception('Failed to load IP ban') + continue + + except(HomeAssistantError, FileNotFoundError): + # No need to report error, file absence means + # that no bans were applied. + return [] + + return ip_list + + +def update_ip_bans_config(path: str, ip_ban: IpBan): + """Update config file with new banned IP address.""" + with open(path, 'a') as out: + ip_ = {str(ip_ban.ip_address): { + ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S") + }} + out.write('\n') + out.write(dump(ip_)) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 787d04a3787..4755c1b03a4 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,6 +1,6 @@ """Helpers for config validation using voluptuous.""" from collections import OrderedDict -from datetime import timedelta +from datetime import timedelta, datetime as datetime_sys import os import re from urllib.parse import urlparse @@ -297,6 +297,22 @@ def time(value): return time_val +def datetime(value): + """Validate datetime.""" + if isinstance(value, datetime_sys): + return value + + try: + date_val = dt_util.parse_datetime(value) + except TypeError: + date_val = None + + if date_val is None: + raise vol.Invalid('Invalid datetime specified: {}'.format(value)) + + return date_val + + def time_zone(value): """Validate timezone.""" if dt_util.get_time_zone(value) is not None: diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 1247d8a0548..82e43300db7 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -124,6 +124,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False resp = yield from client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -155,6 +156,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False resp = yield from client.post(REGISTER_URL, data=json.dumps({ 'browser': 'invalid browser', @@ -209,6 +211,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False resp = yield from client.delete(REGISTER_URL, data=json.dumps({ 'subscription': SUBSCRIPTION_1['subscription'], @@ -253,6 +256,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False resp = yield from client.delete(REGISTER_URL, data=json.dumps({ 'subscription': SUBSCRIPTION_3['subscription'] @@ -295,6 +299,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False with patch('homeassistant.components.notify.html5._save_config', return_value=False): @@ -329,6 +334,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False resp = yield from client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', @@ -384,6 +390,7 @@ class TestHtml5Notify(object): app = web.Application(loop=loop) view.register(app.router) client = yield from test_client(app) + hass.http.is_banned_ip.return_value = False resp = yield from client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 28ded4d6b44..83cda160ac1 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import logging from ipaddress import ip_network -from unittest.mock import patch +from unittest.mock import patch, mock_open import requests @@ -25,7 +25,7 @@ TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1', TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', '2001:DB8:ABCD::1'] UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1'] - +BANNED_IPS = ['200.201.202.203', '100.64.0.1'] CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE] @@ -63,6 +63,9 @@ def setUpModule(): ip_network(trusted_network) for trusted_network in TRUSTED_NETWORKS] + hass.http.ip_bans = [http.IpBan(banned_ip) + for banned_ip in BANNED_IPS] + hass.start() @@ -227,3 +230,56 @@ class TestHttp: assert req.headers.get(allow_origin) == HTTP_BASE_URL assert req.headers.get(allow_headers) == \ const.HTTP_HEADER_HA_AUTH.upper() + + def test_access_from_banned_ip(self): + """Test accessing to server from banned IP. Both trusted and not.""" + hass.http.is_ban_enabled = True + for remote_addr in BANNED_IPS: + with patch('homeassistant.components.http.' + 'HomeAssistantWSGI.get_real_ip', + return_value=remote_addr): + req = requests.get( + _url(const.URL_API)) + assert req.status_code == 403 + + def test_access_from_banned_ip_when_ban_is_off(self): + """Test accessing to server from banned IP when feature is off""" + hass.http.is_ban_enabled = False + for remote_addr in BANNED_IPS: + with patch('homeassistant.components.http.' + 'HomeAssistantWSGI.get_real_ip', + return_value=remote_addr): + req = requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status_code == 200 + + def test_ip_bans_file_creation(self): + """Testing if banned IP file created""" + hass.http.is_ban_enabled = True + hass.http.login_threshold = 1 + + m = mock_open() + + def call_server(): + with patch('homeassistant.components.http.' + 'HomeAssistantWSGI.get_real_ip', + return_value="200.201.202.204"): + return requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: 'Wrong password'}) + + with patch('homeassistant.components.http.open', m, create=True): + req = call_server() + assert req.status_code == 401 + assert len(hass.http.ip_bans) == len(BANNED_IPS) + assert m.call_count == 0 + + req = call_server() + assert req.status_code == 401 + assert len(hass.http.ip_bans) == len(BANNED_IPS) + 1 + m.assert_called_once_with(hass.config.path(http.IP_BANS), 'a') + + req = call_server() + assert req.status_code == 403 + assert m.call_count == 1 diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 072482673d6..60972b7e494 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1,6 +1,6 @@ """Test config validators.""" from collections import OrderedDict -from datetime import timedelta +from datetime import timedelta, datetime, date import enum import os from socket import _GLOBAL_DEFAULT_TIMEOUT @@ -358,6 +358,17 @@ def test_time_zone(): schema('UTC') +def test_datetime(): + """Test date time validation.""" + schema = vol.Schema(cv.datetime) + for value in [date.today(), 'Wrong DateTime', '2016-11-23']: + with pytest.raises(vol.MultipleInvalid): + schema(value) + + schema(datetime.now()) + schema('2016-11-23T18:59:08') + + def test_key_dependency(): """Test key_dependency validator.""" schema = vol.Schema(cv.key_dependency('beer', 'soda')) From 61653a517d965f7c792931c0fcbacb3995d5522b Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 25 Nov 2016 15:03:12 -0500 Subject: [PATCH 058/137] #4421 - Forced icons to be displayed via SSL to avoid Mixed Content warnings (#4544) * #4421 - Forced icons to be displayed via SSL to avoid Mixed Content warnings * Fixed houndci-bot whitespace * Using regex to replace http:// for https:// * Created assert test to verify https translation --- homeassistant/components/sensor/wunderground.py | 4 +++- tests/components/sensor/test_wunderground.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 82bb3d3b245..3194afbe94e 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor.wunderground/ from datetime import timedelta import logging +import re import requests import voluptuous as vol @@ -172,7 +173,8 @@ class WUndergroundSensor(Entity): def entity_picture(self): """Return the entity picture.""" if self._condition == 'weather': - return self.rest.data['icon_url'] + url = self.rest.data['icon_url'] + return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) @property def unit_of_measurement(self): diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index cde94558866..05c7fc93921 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -25,7 +25,7 @@ VALID_CONFIG = { FEELS_LIKE = '40' WEATHER = 'Clear' -ICON_URL = 'http://icons.wxug.com/i/c/k/clear.gif' +HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif' ALERT_MESSAGE = 'This is a test alert message' @@ -61,7 +61,7 @@ def mocked_requests_get(*args, **kwargs): }, "feelslike_c": FEELS_LIKE, "weather": WEATHER, - "icon_url": ICON_URL, + "icon_url": 'http://icons.wxug.com/i/c/k/clear.gif', "display_location": { "city": "Holly Springs", "country": "US", @@ -150,7 +150,7 @@ class TestWundergroundSetup(unittest.TestCase): device.update() self.assertTrue(str(device.name).startswith('PWS_')) if device.name == 'PWS_weather': - self.assertEqual(ICON_URL, device.entity_picture) + self.assertEqual(HTTPS_ICON_URL, device.entity_picture) self.assertEqual(WEATHER, device.state) self.assertIsNone(device.unit_of_measurement) elif device.name == 'PWS_alerts': From 58b85b2e0edb280a7a0f107d5782da45638583ed Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 25 Nov 2016 21:30:53 +0100 Subject: [PATCH 059/137] Upgrade speedtest-cli to 1.0.0 (#4578) --- homeassistant/components/sensor/speedtest.py | 24 +++++++++++--------- requirements_all.txt | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 814e5a1bfa0..3087c74d474 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -1,5 +1,5 @@ """ -Support for Speedtest.net based on speedtest-cli. +Support for Speedtest.net. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.speedtest/ @@ -8,6 +8,7 @@ import logging import re import sys from subprocess import check_output, CalledProcessError + import voluptuous as vol import homeassistant.util.dt as dt_util @@ -18,9 +19,9 @@ from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change -REQUIREMENTS = ['speedtest-cli==0.3.4'] -_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['speedtest-cli==1.0.0'] +_LOGGER = logging.getLogger(__name__) _SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+' r'Download:\s(\d+\.\d+)\sMbit/s[\r\n]+' r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+') @@ -30,6 +31,7 @@ CONF_MINUTE = 'minute' CONF_HOUR = 'hour' CONF_DAY = 'day' CONF_SERVER_ID = 'server_id' + SENSOR_TYPES = { 'ping': ['Ping', 'ms'], 'download': ['Download', 'Mbit/s'], @@ -38,7 +40,7 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]), + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), vol.Optional(CONF_SERVER_ID): cv.positive_int, vol.Optional(CONF_SECOND, default=[0]): vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), @@ -52,12 +54,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Speedtest sensor.""" + """Set up the Speedtest sensor.""" data = SpeedtestData(hass, config) dev = [] for sensor in config[CONF_MONITORED_CONDITIONS]: if sensor not in SENSOR_TYPES: - _LOGGER.error('Sensor type: "%s" does not exist', sensor) + _LOGGER.error("Sensor type: %s does not exist", sensor) else: dev.append(SpeedtestSensor(data, sensor)) @@ -141,18 +143,18 @@ class SpeedtestData(object): def update(self, now): """Get the latest data from speedtest.net.""" - import speedtest_cli + import speedtest - _LOGGER.info('Executing speedtest') + _LOGGER.info('Executing speedtest...') try: - args = [sys.executable, speedtest_cli.__file__, '--simple'] + args = [sys.executable, speedtest.__file__, '--simple'] if self._server_id: args = args + ['--server', str(self._server_id)] re_output = _SPEEDTEST_REGEX.split( - check_output(args).decode("utf-8")) + check_output(args).decode('utf-8')) except CalledProcessError as process_error: - _LOGGER.error('Error executing speedtest: %s', process_error) + _LOGGER.error("Error executing speedtest: %s", process_error) return self.data = {'ping': round(float(re_output[1]), 2), 'download': round(float(re_output[2]), 2), diff --git a/requirements_all.txt b/requirements_all.txt index f9ff30d8ad4..e248829cd4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -510,7 +510,7 @@ snapcast==1.2.2 somecomfort==0.3.2 # homeassistant.components.sensor.speedtest -speedtest-cli==0.3.4 +speedtest-cli==1.0.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator From 32ffd006facea907b2013da19336cf81bd16e6d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Nov 2016 13:04:06 -0800 Subject: [PATCH 060/137] Reorganize HTTP component (#4575) * Move HTTP to own folder * Break HTTP into middlewares * Lint * Split tests per middleware * Clean up HTTP tests * Make HomeAssistantViews more stateless * Lint * Make HTTP setup async --- homeassistant/components/alexa.py | 6 +- homeassistant/components/api.py | 55 +- homeassistant/components/camera/__init__.py | 11 +- .../components/device_tracker/gpslogger.py | 12 +- .../components/device_tracker/locative.py | 17 +- homeassistant/components/emulated_hue.py | 46 +- homeassistant/components/foursquare.py | 8 +- homeassistant/components/frontend/__init__.py | 37 +- homeassistant/components/history.py | 15 +- homeassistant/components/http.py | 641 ------------------ homeassistant/components/http/__init__.py | 407 +++++++++++ homeassistant/components/http/auth.py | 61 ++ homeassistant/components/http/ban.py | 132 ++++ homeassistant/components/http/const.py | 12 + homeassistant/components/http/static.py | 93 +++ homeassistant/components/http/util.py | 25 + homeassistant/components/ios.py | 12 +- homeassistant/components/logbook.py | 8 +- .../components/media_player/__init__.py | 11 +- homeassistant/components/notify/html5.py | 12 +- homeassistant/components/sensor/fitbit.py | 13 +- homeassistant/components/sensor/torque.py | 8 +- homeassistant/components/switch/netio.py | 3 +- homeassistant/const.py | 1 - homeassistant/util/logging.py | 17 + tests/common.py | 17 +- tests/components/camera/test_generic.py | 2 +- tests/components/http/__init__.py | 1 + tests/components/http/test_auth.py | 169 +++++ tests/components/http/test_ban.py | 118 ++++ tests/components/http/test_init.py | 111 +++ tests/components/notify/test_html5.py | 25 +- tests/components/test_frontend.py | 3 +- tests/components/test_http.py | 285 -------- tests/scripts/test_check_config.py | 8 + 35 files changed, 1318 insertions(+), 1084 deletions(-) delete mode 100644 homeassistant/components/http.py create mode 100644 homeassistant/components/http/__init__.py create mode 100644 homeassistant/components/http/auth.py create mode 100644 homeassistant/components/http/ban.py create mode 100644 homeassistant/components/http/const.py create mode 100644 homeassistant/components/http/static.py create mode 100644 homeassistant/components/http/util.py create mode 100644 homeassistant/util/logging.py create mode 100644 tests/components/http/__init__.py create mode 100644 tests/components/http/test_auth.py create mode 100644 tests/components/http/test_ban.py create mode 100644 tests/components/http/test_init.py delete mode 100644 tests/components/test_http.py diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 72c0b2a8705..9bd0d783fee 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -118,7 +118,7 @@ class AlexaIntentsView(HomeAssistantView): def __init__(self, hass, intents): """Initialize Alexa view.""" - super().__init__(hass) + super().__init__() intents = copy.deepcopy(intents) template.attach(hass, intents) @@ -150,7 +150,7 @@ class AlexaIntentsView(HomeAssistantView): return None intent = req.get('intent') - response = AlexaResponse(self.hass, intent) + response = AlexaResponse(request.app['hass'], intent) if req_type == 'LaunchRequest': response.add_speech( @@ -282,7 +282,7 @@ class AlexaFlashBriefingView(HomeAssistantView): def __init__(self, hass, flash_briefings): """Initialize Alexa view.""" - super().__init__(hass) + super().__init__() self.flash_briefings = copy.deepcopy(flash_briefings) template.attach(hass, self.flash_briefings) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ae5e1de7c1b..da8ad9f88ba 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -77,8 +77,10 @@ class APIEventStream(HomeAssistantView): @asyncio.coroutine def get(self, request): """Provide a streaming interface for the event bus.""" + # pylint: disable=no-self-use + hass = request.app['hass'] stop_obj = object() - to_write = asyncio.Queue(loop=self.hass.loop) + to_write = asyncio.Queue(loop=hass.loop) restrict = request.GET.get('restrict') if restrict: @@ -106,7 +108,7 @@ class APIEventStream(HomeAssistantView): response.content_type = 'text/event-stream' yield from response.prepare(request) - unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events) + unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) try: _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) @@ -117,7 +119,7 @@ class APIEventStream(HomeAssistantView): while True: try: with async_timeout.timeout(STREAM_PING_INTERVAL, - loop=self.hass.loop): + loop=hass.loop): payload = yield from to_write.get() if payload is stop_obj: @@ -145,7 +147,7 @@ class APIConfigView(HomeAssistantView): @ha.callback def get(self, request): """Get current configuration.""" - return self.json(self.hass.config.as_dict()) + return self.json(request.app['hass'].config.as_dict()) class APIDiscoveryView(HomeAssistantView): @@ -158,10 +160,11 @@ class APIDiscoveryView(HomeAssistantView): @ha.callback def get(self, request): """Get discovery info.""" - needs_auth = self.hass.config.api.api_password is not None + hass = request.app['hass'] + needs_auth = hass.config.api.api_password is not None return self.json({ - 'base_url': self.hass.config.api.base_url, - 'location_name': self.hass.config.location_name, + 'base_url': hass.config.api.base_url, + 'location_name': hass.config.location_name, 'requires_api_password': needs_auth, 'version': __version__ }) @@ -176,7 +179,7 @@ class APIStatesView(HomeAssistantView): @ha.callback def get(self, request): """Get current states.""" - return self.json(self.hass.states.async_all()) + return self.json(request.app['hass'].states.async_all()) class APIEntityStateView(HomeAssistantView): @@ -188,7 +191,7 @@ class APIEntityStateView(HomeAssistantView): @ha.callback def get(self, request, entity_id): """Retrieve state of entity.""" - state = self.hass.states.get(entity_id) + state = request.app['hass'].states.get(entity_id) if state: return self.json(state) else: @@ -197,6 +200,7 @@ class APIEntityStateView(HomeAssistantView): @asyncio.coroutine def post(self, request, entity_id): """Update state of entity.""" + hass = request.app['hass'] try: data = yield from request.json() except ValueError: @@ -211,15 +215,14 @@ class APIEntityStateView(HomeAssistantView): attributes = data.get('attributes') force_update = data.get('force_update', False) - is_new_state = self.hass.states.get(entity_id) is None + is_new_state = hass.states.get(entity_id) is None # Write state - self.hass.states.async_set(entity_id, new_state, attributes, - force_update) + hass.states.async_set(entity_id, new_state, attributes, force_update) # Read the state back for our response status_code = HTTP_CREATED if is_new_state else 200 - resp = self.json(self.hass.states.get(entity_id), status_code) + resp = self.json(hass.states.get(entity_id), status_code) resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) @@ -228,7 +231,7 @@ class APIEntityStateView(HomeAssistantView): @ha.callback def delete(self, request, entity_id): """Remove entity.""" - if self.hass.states.async_remove(entity_id): + if request.app['hass'].states.async_remove(entity_id): return self.json_message('Entity removed') else: return self.json_message('Entity not found', HTTP_NOT_FOUND) @@ -243,7 +246,7 @@ class APIEventListenersView(HomeAssistantView): @ha.callback def get(self, request): """Get event listeners.""" - return self.json(async_events_json(self.hass)) + return self.json(async_events_json(request.app['hass'])) class APIEventView(HomeAssistantView): @@ -271,7 +274,8 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote) + request.app['hass'].bus.async_fire(event_type, event_data, + ha.EventOrigin.remote) return self.json_message("Event {} fired.".format(event_type)) @@ -285,7 +289,7 @@ class APIServicesView(HomeAssistantView): @ha.callback def get(self, request): """Get registered services.""" - return self.json(async_services_json(self.hass)) + return self.json(async_services_json(request.app['hass'])) class APIDomainServicesView(HomeAssistantView): @@ -300,12 +304,12 @@ class APIDomainServicesView(HomeAssistantView): Returns a list of changed states. """ + hass = request.app['hass'] body = yield from request.text() data = json.loads(body) if body else None - with AsyncTrackStates(self.hass) as changed_states: - yield from self.hass.services.async_call(domain, service, data, - True) + with AsyncTrackStates(hass) as changed_states: + yield from hass.services.async_call(domain, service, data, True) return self.json(changed_states) @@ -320,6 +324,7 @@ class APIEventForwardingView(HomeAssistantView): @asyncio.coroutine def post(self, request): """Setup an event forwarder.""" + hass = request.app['hass'] try: data = yield from request.json() except ValueError: @@ -340,14 +345,14 @@ class APIEventForwardingView(HomeAssistantView): api = rem.API(host, api_password, port) - valid = yield from self.hass.loop.run_in_executor( + valid = yield from hass.loop.run_in_executor( None, api.validate_api) if not valid: return self.json_message("Unable to validate API.", HTTP_UNPROCESSABLE_ENTITY) if self.event_forwarder is None: - self.event_forwarder = rem.EventForwarder(self.hass) + self.event_forwarder = rem.EventForwarder(hass) self.event_forwarder.async_connect(api) @@ -389,7 +394,7 @@ class APIComponentsView(HomeAssistantView): @ha.callback def get(self, request): """Get current loaded components.""" - return self.json(self.hass.config.components) + return self.json(request.app['hass'].config.components) class APIErrorLogView(HomeAssistantView): @@ -402,7 +407,7 @@ class APIErrorLogView(HomeAssistantView): def get(self, request): """Serve error log.""" resp = yield from self.file( - request, self.hass.config.path(ERROR_LOG_FILENAME)) + request, request.app['hass'].config.path(ERROR_LOG_FILENAME)) return resp @@ -417,7 +422,7 @@ class APITemplateView(HomeAssistantView): """Render a template.""" try: data = yield from request.json() - tpl = template.Template(data['template'], self.hass) + tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: return self.json_message('Error rendering template: {}'.format(ex), diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 6724598419f..427d4535ef6 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -13,7 +13,7 @@ from aiohttp import web from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED DOMAIN = 'camera' DEPENDENCIES = ['http'] @@ -33,8 +33,8 @@ def async_setup(hass, config): component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.http.register_view(CameraImageView(hass, component.entities)) - hass.http.register_view(CameraMjpegStream(hass, component.entities)) + hass.http.register_view(CameraImageView(component.entities)) + hass.http.register_view(CameraMjpegStream(component.entities)) yield from component.async_setup(config) return True @@ -165,9 +165,8 @@ class CameraView(HomeAssistantView): requires_auth = False - def __init__(self, hass, entities): + def __init__(self, entities): """Initialize a basic camera view.""" - super().__init__(hass) self.entities = entities @asyncio.coroutine @@ -178,7 +177,7 @@ class CameraView(HomeAssistantView): if camera is None: return web.Response(status=404) - authenticated = (request.authenticated or + authenticated = (request[KEY_AUTHENTICATED] or request.GET.get('token') == camera.access_token) if not authenticated: diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 462ae16300c..2e897ccb10c 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -21,7 +21,7 @@ DEPENDENCIES = ['http'] def setup_scanner(hass, config, see): """Setup an endpoint for the GPSLogger application.""" - hass.http.register_view(GPSLoggerView(hass, see)) + hass.http.register_view(GPSLoggerView(see)) return True @@ -32,20 +32,18 @@ class GPSLoggerView(HomeAssistantView): url = '/api/gpslogger' name = 'api:gpslogger' - def __init__(self, hass, see): + def __init__(self, see): """Initialize GPSLogger url endpoints.""" - super().__init__(hass) self.see = see @asyncio.coroutine def get(self, request): """A GPSLogger message received as GET.""" - res = yield from self._handle(request.GET) + res = yield from self._handle(request.app['hass'], request.GET) return res @asyncio.coroutine - # pylint: disable=too-many-return-statements - def _handle(self, data): + def _handle(self, hass, data): """Handle gpslogger request.""" if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', @@ -66,7 +64,7 @@ class GPSLoggerView(HomeAssistantView): if 'battery' in data: battery = float(data['battery']) - yield from self.hass.loop.run_in_executor( + yield from hass.loop.run_in_executor( None, partial(self.see, dev_id=device, gps=gps_location, battery=battery, gps_accuracy=accuracy)) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index e6bd74e57c9..10641b3a921 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -23,7 +23,7 @@ DEPENDENCIES = ['http'] def setup_scanner(hass, config, see): """Setup an endpoint for the Locative application.""" - hass.http.register_view(LocativeView(hass, see)) + hass.http.register_view(LocativeView(see)) return True @@ -34,27 +34,26 @@ class LocativeView(HomeAssistantView): url = '/api/locative' name = 'api:locative' - def __init__(self, hass, see): + def __init__(self, see): """Initialize Locative url endpoints.""" - super().__init__(hass) self.see = see @asyncio.coroutine def get(self, request): """Locative message received as GET.""" - res = yield from self._handle(request.GET) + res = yield from self._handle(request.app['hass'], request.GET) return res @asyncio.coroutine def post(self, request): """Locative message received.""" data = yield from request.post() - res = yield from self._handle(data) + res = yield from self._handle(request.app['hass'], data) return res @asyncio.coroutine # pylint: disable=too-many-return-statements - def _handle(self, data): + def _handle(self, hass, data): """Handle locative request.""" if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', @@ -81,19 +80,19 @@ class LocativeView(HomeAssistantView): gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': - yield from self.hass.loop.run_in_executor( + yield from hass.loop.run_in_executor( None, partial(self.see, dev_id=device, location_name=location_name, gps=gps_location)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': - current_state = self.hass.states.get( + current_state = hass.states.get( '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME - yield from self.hass.loop.run_in_executor( + yield from hass.loop.run_in_executor( None, partial(self.see, dev_id=device, location_name=location_name, gps=gps_location)) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index afb5c63918c..dcb6bcb64b2 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -78,14 +78,13 @@ def setup(hass, yaml_config): cors_origins=None, use_x_forwarded_for=False, trusted_networks=None, - ip_bans=None, login_threshold=0, is_ban_enabled=False ) - server.register_view(DescriptionXmlView(hass, config)) - server.register_view(HueUsernameView(hass)) - server.register_view(HueLightsView(hass, config)) + server.register_view(DescriptionXmlView(config)) + server.register_view(HueUsernameView) + server.register_view(HueLightsView(config)) upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port) @@ -157,9 +156,8 @@ class DescriptionXmlView(HomeAssistantView): name = 'description:xml' requires_auth = False - def __init__(self, hass, config): + def __init__(self, config): """Initialize the instance of the view.""" - super().__init__(hass) self.config = config @core.callback @@ -201,10 +199,6 @@ class HueUsernameView(HomeAssistantView): extra_urls = ['/api/'] requires_auth = False - def __init__(self, hass): - """Initialize the instance of the view.""" - super().__init__(hass) - @asyncio.coroutine def post(self, request): """Handle a POST request.""" @@ -229,30 +223,33 @@ class HueLightsView(HomeAssistantView): '/api/{username}/lights/{entity_id}/state'] requires_auth = False - def __init__(self, hass, config): + def __init__(self, config): """Initialize the instance of the view.""" - super().__init__(hass) self.config = config self.cached_states = {} @core.callback def get(self, request, username, entity_id=None): """Handle a GET request.""" + hass = request.app['hass'] + if entity_id is None: - return self.async_get_lights_list() + return self.async_get_lights_list(hass) if not request.path.endswith('state'): - return self.async_get_light_state(entity_id) + return self.async_get_light_state(hass, entity_id) return web.Response(text="Method not allowed", status=405) @asyncio.coroutine def put(self, request, username, entity_id=None): """Handle a PUT request.""" + hass = request.app['hass'] + if not request.path.endswith('state'): return web.Response(text="Method not allowed", status=405) - if entity_id and self.hass.states.get(entity_id) is None: + if entity_id and hass.states.get(entity_id) is None: return self.json_message('Entity not found', HTTP_NOT_FOUND) try: @@ -260,24 +257,25 @@ class HueLightsView(HomeAssistantView): except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) - result = yield from self.async_put_light_state(json_data, entity_id) + result = yield from self.async_put_light_state(hass, json_data, + entity_id) return result @core.callback - def async_get_lights_list(self): + def async_get_lights_list(self, hass): """Process a request to get the list of available lights.""" json_response = {} - for entity in self.hass.states.async_all(): + for entity in hass.states.async_all(): if self.is_entity_exposed(entity): json_response[entity.entity_id] = entity_to_json(entity) return self.json(json_response) @core.callback - def async_get_light_state(self, entity_id): + def async_get_light_state(self, hass, entity_id): """Process a request to get the state of an individual light.""" - entity = self.hass.states.get(entity_id) + entity = hass.states.get(entity_id) if entity is None or not self.is_entity_exposed(entity): return web.Response(text="Entity not found", status=404) @@ -295,12 +293,12 @@ class HueLightsView(HomeAssistantView): return self.json(json_response) @asyncio.coroutine - def async_put_light_state(self, request_json, entity_id): + def async_put_light_state(self, hass, request_json, entity_id): """Process a request to set the state of an individual light.""" config = self.config # Retrieve the entity from the state machine - entity = self.hass.states.get(entity_id) + entity = hass.states.get(entity_id) if entity is None: return web.Response(text="Entity not found", status=404) @@ -345,8 +343,8 @@ class HueLightsView(HomeAssistantView): self.cached_states[entity_id] = (result, brightness) # Perform the requested action - yield from self.hass.services.async_call(core.DOMAIN, service, data, - blocking=True) + yield from hass.services.async_call(core.DOMAIN, service, data, + blocking=True) json_response = \ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index bb4c66ad1f9..2afa808b502 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -75,8 +75,7 @@ def setup(hass, config): descriptions[DOMAIN][SERVICE_CHECKIN], schema=CHECKIN_SERVICE_SCHEMA) - hass.http.register_view(FoursquarePushReceiver( - hass, config[CONF_PUSH_SECRET])) + hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET])) return True @@ -88,9 +87,8 @@ class FoursquarePushReceiver(HomeAssistantView): url = "/api/foursquare" name = "foursquare" - def __init__(self, hass, push_secret): + def __init__(self, push_secret): """Initialize the OAuth callback view.""" - super().__init__(hass) self.push_secret = push_secret @asyncio.coroutine @@ -110,4 +108,4 @@ class FoursquarePushReceiver(HomeAssistantView): "push secret: %s", secret) return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) - self.hass.bus.async_fire(EVENT_PUSH, data) + request.app['hass'].bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 6fde1ae388a..e19e5f6edec 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -11,6 +11,8 @@ from homeassistant.core import callback from homeassistant.const import HTTP_NOT_FOUND from homeassistant.components import api, group from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.auth import is_trusted_ip +from homeassistant.components.http.const import KEY_DEVELOPMENT from .version import FINGERPRINTS DOMAIN = 'frontend' @@ -155,7 +157,7 @@ def setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local) - index_view = hass.data[DATA_INDEX_VIEW] = IndexView(hass) + index_view = hass.data[DATA_INDEX_VIEW] = IndexView() hass.http.register_view(index_view) # Components have registered panels before frontend got setup. @@ -185,12 +187,14 @@ class BootstrapView(HomeAssistantView): @callback def get(self, request): """Return all data needed to bootstrap Home Assistant.""" + hass = request.app['hass'] + return self.json({ - 'config': self.hass.config.as_dict(), - 'states': self.hass.states.async_all(), - 'events': api.async_events_json(self.hass), - 'services': api.async_services_json(self.hass), - 'panels': self.hass.data[DATA_PANELS], + 'config': hass.config.as_dict(), + 'states': hass.states.async_all(), + 'events': api.async_events_json(hass), + 'services': api.async_services_json(hass), + 'panels': hass.data[DATA_PANELS], }) @@ -202,10 +206,8 @@ class IndexView(HomeAssistantView): requires_auth = False extra_urls = ['/states', '/states/{entity_id}'] - def __init__(self, hass): + def __init__(self): """Initialize the frontend view.""" - super().__init__(hass) - from jinja2 import FileSystemLoader, Environment self.templates = Environment( @@ -217,14 +219,16 @@ class IndexView(HomeAssistantView): @asyncio.coroutine def get(self, request, entity_id=None): """Serve the index view.""" + hass = request.app['hass'] + if entity_id is not None: - state = self.hass.states.get(entity_id) + state = hass.states.get(entity_id) if (not state or state.domain != 'group' or not state.attributes.get(group.ATTR_VIEW)): return self.json_message('Entity not found', HTTP_NOT_FOUND) - if self.hass.http.development: + if request.app[KEY_DEVELOPMENT]: core_url = '/static/home-assistant-polymer/build/core.js' ui_url = '/static/home-assistant-polymer/src/home-assistant.html' else: @@ -241,19 +245,18 @@ class IndexView(HomeAssistantView): if panel == 'states': panel_url = '' else: - panel_url = self.hass.data[DATA_PANELS][panel]['url'] + panel_url = hass.data[DATA_PANELS][panel]['url'] no_auth = 'true' - if self.hass.config.api.api_password: + if hass.config.api.api_password: # require password if set no_auth = 'false' - if self.hass.http.is_trusted_ip( - self.hass.http.get_real_ip(request)): + if is_trusted_ip(request): # bypass for trusted networks no_auth = 'true' icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) - template = yield from self.hass.loop.run_in_executor( + template = yield from hass.loop.run_in_executor( None, self.templates.get_template, 'index.html') # pylint is wrong @@ -262,7 +265,7 @@ class IndexView(HomeAssistantView): resp = template.render( core_url=core_url, ui_url=ui_url, no_auth=no_auth, icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], - panel_url=panel_url, panels=self.hass.data[DATA_PANELS]) + panel_url=panel_url, panels=hass.data[DATA_PANELS]) return web.Response(text=resp, content_type='text/html') diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index c3dd0bd3f5a..eee0570c9bc 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -184,8 +184,8 @@ def setup(hass, config): filters.included_entities = include[CONF_ENTITIES] filters.included_domains = include[CONF_DOMAINS] - hass.http.register_view(Last5StatesView(hass)) - hass.http.register_view(HistoryPeriodView(hass, filters)) + hass.http.register_view(Last5StatesView) + hass.http.register_view(HistoryPeriodView(filters)) register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') return True @@ -197,14 +197,10 @@ class Last5StatesView(HomeAssistantView): url = '/api/history/entity/{entity_id}/recent_states' name = 'api:history:entity-recent-states' - def __init__(self, hass): - """Initilalize the history last 5 states view.""" - super().__init__(hass) - @asyncio.coroutine def get(self, request, entity_id): """Retrieve last 5 states of entity.""" - result = yield from self.hass.loop.run_in_executor( + result = yield from request.app['hass'].loop.run_in_executor( None, last_5_states, entity_id) return self.json(result) @@ -216,9 +212,8 @@ class HistoryPeriodView(HomeAssistantView): name = 'api:history:view-period' extra_urls = ['/api/history/period/{datetime}'] - def __init__(self, hass, filters): + def __init__(self, filters): """Initilalize the history period view.""" - super().__init__(hass) self.filters = filters @asyncio.coroutine @@ -240,7 +235,7 @@ class HistoryPeriodView(HomeAssistantView): end_time = start_time + one_day entity_id = request.GET.get('filter_entity_id') - result = yield from self.hass.loop.run_in_executor( + result = yield from request.app['hass'].loop.run_in_executor( None, get_significant_states, start_time, end_time, entity_id, self.filters) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py deleted file mode 100644 index 054d8050599..00000000000 --- a/homeassistant/components/http.py +++ /dev/null @@ -1,641 +0,0 @@ -""" -This module provides WSGI application to serve the Home Assistant API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/http/ -""" -import asyncio -import json -import logging -import mimetypes -import ssl -from datetime import datetime -from ipaddress import ip_address, ip_network -from pathlib import Path - -import hmac -import os -import re -import voluptuous as vol -from aiohttp import web, hdrs -from aiohttp.file_sender import FileSender -from aiohttp.web_exceptions import ( - HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified, HTTPForbidden) -from aiohttp.web_urldispatcher import StaticResource - -import homeassistant.helpers.config_validation as cv -import homeassistant.remote as rem -from homeassistant import util -from homeassistant.components import persistent_notification -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ( - SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL, - CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR) -from homeassistant.core import is_callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.yaml import dump - -DOMAIN = 'http' -REQUIREMENTS = ('aiohttp_cors==0.5.0',) - -CONF_API_PASSWORD = 'api_password' -CONF_SERVER_HOST = 'server_host' -CONF_SERVER_PORT = 'server_port' -CONF_DEVELOPMENT = 'development' -CONF_SSL_CERTIFICATE = 'ssl_certificate' -CONF_SSL_KEY = 'ssl_key' -CONF_CORS_ORIGINS = 'cors_allowed_origins' -CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' -CONF_TRUSTED_NETWORKS = 'trusted_networks' -CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' -CONF_IP_BAN_ENABLED = 'ip_ban_enabled' - -DATA_API_PASSWORD = 'api_password' -NOTIFICATION_ID_LOGIN = 'http-login' -NOTIFICATION_ID_BAN = 'ip-ban' - -IP_BANS = 'ip_bans.yaml' -ATTR_BANNED_AT = "banned_at" - - -# TLS configuation follows the best-practice guidelines specified here: -# https://wiki.mozilla.org/Security/Server_Side_TLS -# Intermediate guidelines are followed. -SSL_VERSION = ssl.PROTOCOL_SSLv23 -SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 -if hasattr(ssl, 'OP_NO_COMPRESSION'): - SSL_OPTS |= ssl.OP_NO_COMPRESSION -CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ - "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ - "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ - "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ - "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ - "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ - "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ - "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ - "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ - "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ - "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ - "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ - "AES256-SHA:DES-CBC3-SHA:!DSS" - -_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_API_PASSWORD): cv.string, - vol.Optional(CONF_SERVER_HOST): cv.string, - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), - vol.Optional(CONF_DEVELOPMENT): cv.string, - vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_KEY): cv.isfile, - vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, - vol.Optional(CONF_TRUSTED_NETWORKS): - vol.All(cv.ensure_list, [ip_network]), - vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD): cv.positive_int, - vol.Optional(CONF_IP_BAN_ENABLED): cv.boolean - }), -}, extra=vol.ALLOW_EXTRA) - - -# TEMP TO GET TESTS TO RUN -def request_class(): - """.""" - raise Exception('not implemented') - - -class HideSensitiveFilter(logging.Filter): - """Filter API password calls.""" - - def __init__(self, hass): - """Initialize sensitive data filter.""" - super().__init__() - self.hass = hass - - def filter(self, record): - """Hide sensitive data in messages.""" - if self.hass.http.api_password is None: - return True - - record.msg = record.msg.replace(self.hass.http.api_password, '*******') - - return True - - -def setup(hass, config): - """Set up the HTTP API and debug interface.""" - logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass)) - - conf = config.get(DOMAIN, {}) - - api_password = util.convert(conf.get(CONF_API_PASSWORD), str) - server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0') - server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) - development = str(conf.get(CONF_DEVELOPMENT, '')) == '1' - ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) - ssl_key = conf.get(CONF_SSL_KEY) - cors_origins = conf.get(CONF_CORS_ORIGINS, []) - use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) - trusted_networks = [ - ip_network(trusted_network) - for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])] - is_ban_enabled = bool(conf.get(CONF_IP_BAN_ENABLED, False)) - login_threshold = int(conf.get(CONF_LOGIN_ATTEMPTS_THRESHOLD, -1)) - ip_bans = load_ip_bans_config(hass.config.path(IP_BANS)) - - server = HomeAssistantWSGI( - hass, - development=development, - server_host=server_host, - server_port=server_port, - api_password=api_password, - ssl_certificate=ssl_certificate, - ssl_key=ssl_key, - cors_origins=cors_origins, - use_x_forwarded_for=use_x_forwarded_for, - trusted_networks=trusted_networks, - ip_bans=ip_bans, - login_threshold=login_threshold, - is_ban_enabled=is_ban_enabled - ) - - @asyncio.coroutine - def stop_server(event): - """Callback to stop the server.""" - yield from server.stop() - - @asyncio.coroutine - def start_server(event): - """Callback to start the server.""" - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) - yield from server.start() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server) - - hass.http = server - hass.config.api = rem.API(server_host if server_host != '0.0.0.0' - else util.get_local_ip(), - api_password, server_port, - ssl_certificate is not None) - - return True - - -class GzipFileSender(FileSender): - """FileSender class capable of sending gzip version if available.""" - - # pylint: disable=invalid-name - - development = False - - @asyncio.coroutine - def send(self, request, filepath): - """Send filepath to client using request.""" - gzip = False - if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]: - gzip_path = filepath.with_name(filepath.name + '.gz') - - if gzip_path.is_file(): - filepath = gzip_path - gzip = True - - st = filepath.stat() - - modsince = request.if_modified_since - if modsince is not None and st.st_mtime <= modsince.timestamp(): - raise HTTPNotModified() - - ct, encoding = mimetypes.guess_type(str(filepath)) - if not ct: - ct = 'application/octet-stream' - - resp = self._response_factory() - resp.content_type = ct - if encoding: - resp.headers[hdrs.CONTENT_ENCODING] = encoding - if gzip: - resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING - resp.last_modified = st.st_mtime - - # CACHE HACK - if not self.development: - cache_time = 31 * 86400 # = 1 month - resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( - cache_time) - - file_size = st.st_size - - resp.content_length = file_size - with filepath.open('rb') as f: - yield from self._sendfile(request, resp, f, file_size) - - return resp - - -_GZIP_FILE_SENDER = GzipFileSender() - - -@asyncio.coroutine -def staticresource_enhancer(app, handler): - """Enhance StaticResourceHandler. - - Adds gzip encoding and fingerprinting matching. - """ - inst = getattr(handler, '__self__', None) - if not isinstance(inst, StaticResource): - return handler - - # pylint: disable=protected-access - inst._file_sender = _GZIP_FILE_SENDER - - @asyncio.coroutine - def middleware_handler(request): - """Strip out fingerprints from resource names.""" - fingerprinted = _FINGERPRINT.match(request.match_info['filename']) - - if fingerprinted: - request.match_info['filename'] = \ - '{}.{}'.format(*fingerprinted.groups()) - - resp = yield from handler(request) - return resp - - return middleware_handler - - -class HomeAssistantWSGI(object): - """WSGI server for Home Assistant.""" - - def __init__(self, hass, development, api_password, ssl_certificate, - ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_networks, - ip_bans, login_threshold, is_ban_enabled): - """Initialize the WSGI Home Assistant server.""" - import aiohttp_cors - - self.app = web.Application(middlewares=[staticresource_enhancer], - loop=hass.loop) - self.hass = hass - self.development = development - self.api_password = api_password - self.ssl_certificate = ssl_certificate - self.ssl_key = ssl_key - self.server_host = server_host - self.server_port = server_port - self.use_x_forwarded_for = use_x_forwarded_for - self.trusted_networks = trusted_networks \ - if trusted_networks is not None else [] - self.event_forwarder = None - self._handler = None - self.server = None - self.login_threshold = login_threshold - self.ip_bans = ip_bans if ip_bans is not None else [] - self.failed_login_attempts = {} - self.is_ban_enabled = is_ban_enabled - - if cors_origins: - self.cors = aiohttp_cors.setup(self.app, defaults={ - host: aiohttp_cors.ResourceOptions( - allow_headers=ALLOWED_CORS_HEADERS, - allow_methods='*', - ) for host in cors_origins - }) - else: - self.cors = None - - # CACHE HACK - _GZIP_FILE_SENDER.development = development - - def register_view(self, view): - """Register a view with the WSGI server. - - The view argument must be a class that inherits from HomeAssistantView. - It is optional to instantiate it before registering; this method will - handle it either way. - """ - if isinstance(view, type): - # Instantiate the view, if needed - view = view(self.hass) - - view.register(self.app.router) - - def register_redirect(self, url, redirect_to): - """Register a redirect with the server. - - If given this must be either a string or callable. In case of a - callable it's called with the url adapter that triggered the match and - the values of the URL as keyword arguments and has to return the target - for the redirect, otherwise it has to be a string with placeholders in - rule syntax. - """ - def redirect(request): - """Redirect to location.""" - raise HTTPMovedPermanently(redirect_to) - - self.app.router.add_route('GET', url, redirect) - - def register_static_path(self, url_root, path, cache_length=31): - """Register a folder to serve as a static path. - - Specify optional cache length of asset in days. - """ - if os.path.isdir(path): - self.app.router.add_static(url_root, path) - return - - filepath = Path(path) - - @asyncio.coroutine - def serve_file(request): - """Redirect to location.""" - res = yield from _GZIP_FILE_SENDER.send(request, filepath) - return res - - # aiohttp supports regex matching for variables. Using that as temp - # to work around cache busting MD5. - # Turns something like /static/dev-panel.html into - # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} - base, ext = url_root.rsplit('.', 1) - base, file = base.rsplit('/', 1) - regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext) - url_pattern = "{}/{{filename:{}}}".format(base, regex) - - self.app.router.add_route('GET', url_pattern, serve_file) - - @asyncio.coroutine - def start(self): - """Start the wsgi server.""" - if self.cors is not None: - for route in list(self.app.router.routes()): - self.cors.add(route) - - if self.ssl_certificate: - context = ssl.SSLContext(SSL_VERSION) - context.options |= SSL_OPTS - context.set_ciphers(CIPHERS) - context.load_cert_chain(self.ssl_certificate, self.ssl_key) - else: - context = None - - self._handler = self.app.make_handler() - self.server = yield from self.hass.loop.create_server( - self._handler, self.server_host, self.server_port, ssl=context) - - @asyncio.coroutine - def stop(self): - """Stop the wsgi server.""" - self.server.close() - yield from self.server.wait_closed() - yield from self.app.shutdown() - yield from self._handler.finish_connections(60.0) - yield from self.app.cleanup() - - def get_real_ip(self, request): - """Return the clients correct ip address, even in proxied setups.""" - if self.use_x_forwarded_for \ - and HTTP_HEADER_X_FORWARDED_FOR in request.headers: - return request.headers.get( - HTTP_HEADER_X_FORWARDED_FOR).split(',')[0] - else: - peername = request.transport.get_extra_info('peername') - return peername[0] if peername is not None else None - - def is_trusted_ip(self, remote_addr): - """Match an ip address against trusted CIDR networks.""" - return any(ip_address(remote_addr) in trusted_network - for trusted_network in self.hass.http.trusted_networks) - - def wrong_login_attempt(self, remote_addr): - """Registering wrong login attempt.""" - if not self.is_ban_enabled or self.login_threshold < 1: - return - - if remote_addr in self.failed_login_attempts: - self.failed_login_attempts[remote_addr] += 1 - else: - self.failed_login_attempts[remote_addr] = 1 - - if self.failed_login_attempts[remote_addr] > self.login_threshold: - new_ban = IpBan(remote_addr) - self.ip_bans.append(new_ban) - update_ip_bans_config(self.hass.config.path(IP_BANS), new_ban) - _LOGGER.warning('Banned IP %s for too many login attempts', - remote_addr) - persistent_notification.async_create( - self.hass, - 'Too many login attempts from {}'.format(remote_addr), - 'Banning IP address', NOTIFICATION_ID_BAN) - - def is_banned_ip(self, remote_addr): - """Check if IP address is in a ban list.""" - if not self.is_ban_enabled: - return False - - ip_address_ = ip_address(remote_addr) - for ip_ban in self.ip_bans: - if ip_ban.ip_address == ip_address_: - return True - - return False - - -class HomeAssistantView(object): - """Base view for all views.""" - - url = None - extra_urls = [] - requires_auth = True # Views inheriting from this class can override this - - def __init__(self, hass): - """Initilalize the base view.""" - if not hasattr(self, 'url'): - class_name = self.__class__.__name__ - raise AttributeError( - '{0} missing required attribute "url"'.format(class_name) - ) - - if not hasattr(self, 'name'): - class_name = self.__class__.__name__ - raise AttributeError( - '{0} missing required attribute "name"'.format(class_name) - ) - - self.hass = hass - - # pylint: disable=no-self-use - def json(self, result, status_code=200): - """Return a JSON response.""" - msg = json.dumps( - result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') - return web.Response( - body=msg, content_type=CONTENT_TYPE_JSON, status=status_code) - - def json_message(self, error, status_code=200): - """Return a JSON message response.""" - return self.json({'message': error}, status_code) - - @asyncio.coroutine - # pylint: disable=no-self-use - def file(self, request, fil): - """Return a file.""" - assert isinstance(fil, str), 'only string paths allowed' - response = yield from _GZIP_FILE_SENDER.send(request, Path(fil)) - return response - - def register(self, router): - """Register the view with a router.""" - assert self.url is not None, 'No url set for view' - urls = [self.url] + self.extra_urls - - for method in ('get', 'post', 'delete', 'put'): - handler = getattr(self, method, None) - - if not handler: - continue - - handler = request_handler_factory(self, handler) - - for url in urls: - router.add_route(method, url, handler) - - # aiohttp_cors does not work with class based views - # self.app.router.add_route('*', self.url, self, name=self.name) - - # for url in self.extra_urls: - # self.app.router.add_route('*', url, self) - - -def request_handler_factory(view, handler): - """Factory to wrap our handler classes. - - Eventually authentication should be managed by middleware. - """ - @asyncio.coroutine - def handle(request): - """Handle incoming request.""" - if not view.hass.is_running: - return web.Response(status=503) - - remote_addr = view.hass.http.get_real_ip(request) - - if view.hass.http.is_banned_ip(remote_addr): - raise HTTPForbidden() - - # Auth code verbose on purpose - authenticated = False - - if view.hass.http.api_password is None: - authenticated = True - - elif view.hass.http.is_trusted_ip(remote_addr): - authenticated = True - - elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''), - view.hass.http.api_password): - # A valid auth header has been set - authenticated = True - - elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''), - view.hass.http.api_password): - authenticated = True - - if view.requires_auth and not authenticated: - view.hass.http.wrong_login_attempt(remote_addr) - _LOGGER.warning('Login attempt or request with an invalid ' - 'password from %s', remote_addr) - persistent_notification.async_create( - view.hass, - 'Invalid password used from {}'.format(remote_addr), - 'Login attempt failed', NOTIFICATION_ID_LOGIN) - raise HTTPUnauthorized() - - request.authenticated = authenticated - - _LOGGER.info('Serving %s to %s (auth: %s)', - request.path, remote_addr, authenticated) - - assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \ - "Handler should be a coroutine or a callback." - - result = handler(request, **request.match_info) - - if asyncio.iscoroutine(result): - result = yield from result - - if isinstance(result, web.StreamResponse): - # The method handler returned a ready-made Response, how nice of it - return result - - status_code = 200 - - if isinstance(result, tuple): - result, status_code = result - - if isinstance(result, str): - result = result.encode('utf-8') - elif result is None: - result = b'' - elif not isinstance(result, bytes): - assert False, ('Result should be None, string, bytes or Response. ' - 'Got: {}').format(result) - - return web.Response(body=result, status=status_code) - - return handle - - -class IpBan(object): - """Represents banned IP address.""" - - def __init__(self, ip_ban: str, banned_at: datetime=None) -> None: - """Initializing Ip Ban object.""" - self.ip_address = ip_address(ip_ban) - self.banned_at = banned_at - if self.banned_at is None: - self.banned_at = datetime.utcnow() - - -def load_ip_bans_config(path: str): - """Loading list of banned IPs from config file.""" - ip_list = [] - ip_schema = vol.Schema({ - vol.Optional('banned_at'): vol.Any(None, cv.datetime) - }) - - try: - try: - list_ = load_yaml_config_file(path) - except HomeAssistantError as err: - _LOGGER.error('Unable to load %s: %s', path, str(err)) - return [] - - for ip_ban, ip_info in list_.items(): - try: - ip_info = ip_schema(ip_info) - ip_info['ip_ban'] = ip_address(ip_ban) - ip_list.append(IpBan(**ip_info)) - except vol.Invalid: - _LOGGER.exception('Failed to load IP ban') - continue - - except(HomeAssistantError, FileNotFoundError): - # No need to report error, file absence means - # that no bans were applied. - return [] - - return ip_list - - -def update_ip_bans_config(path: str, ip_ban: IpBan): - """Update config file with new banned IP address.""" - with open(path, 'a') as out: - ip_ = {str(ip_ban.ip_address): { - ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S") - }} - out.write('\n') - out.write(dump(ip_)) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py new file mode 100644 index 00000000000..0404f4a0df6 --- /dev/null +++ b/homeassistant/components/http/__init__.py @@ -0,0 +1,407 @@ +""" +This module provides WSGI application to serve the Home Assistant API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/http/ +""" +import asyncio +import json +import logging +import ssl +from ipaddress import ip_network +from pathlib import Path + +import os +import voluptuous as vol +from aiohttp import web +from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently + +import homeassistant.helpers.config_validation as cv +import homeassistant.remote as rem +from homeassistant.util import get_local_ip +from homeassistant.components import persistent_notification +from homeassistant.const import ( + SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) +from homeassistant.core import is_callback +from homeassistant.util.logging import HideSensitiveDataFilter + +from .auth import auth_middleware +from .ban import ban_middleware, process_wrong_login +from .const import ( + KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS, + KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD, + KEY_DEVELOPMENT, KEY_AUTHENTICATED) +from .static import GZIP_FILE_SENDER, staticresource_middleware +from .util import get_real_ip + +DOMAIN = 'http' +REQUIREMENTS = ('aiohttp_cors==0.5.0',) + +CONF_API_PASSWORD = 'api_password' +CONF_SERVER_HOST = 'server_host' +CONF_SERVER_PORT = 'server_port' +CONF_DEVELOPMENT = 'development' +CONF_SSL_CERTIFICATE = 'ssl_certificate' +CONF_SSL_KEY = 'ssl_key' +CONF_CORS_ORIGINS = 'cors_allowed_origins' +CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' +CONF_TRUSTED_NETWORKS = 'trusted_networks' +CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' +CONF_IP_BAN_ENABLED = 'ip_ban_enabled' + +NOTIFICATION_ID_LOGIN = 'http-login' + +# TLS configuation follows the best-practice guidelines specified here: +# https://wiki.mozilla.org/Security/Server_Side_TLS +# Intermediate guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_SSLv23 +SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 +if hasattr(ssl, 'OP_NO_COMPRESSION'): + SSL_OPTS |= ssl.OP_NO_COMPRESSION +CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ + "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ + "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ + "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ + "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ + "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ + "AES256-SHA:DES-CBC3-SHA:!DSS" + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SERVER_HOST = '0.0.0.0' +DEFAULT_DEVELOPMENT = '0' +DEFAULT_LOGIN_ATTEMPT_THRESHOLD = -1 + +HTTP_SCHEMA = vol.Schema({ + vol.Optional(CONF_API_PASSWORD, default=None): cv.string, + vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, + vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_DEVELOPMENT, default=DEFAULT_DEVELOPMENT): cv.string, + vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile, + vol.Optional(CONF_SSL_KEY, default=None): cv.isfile, + vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, + vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): + vol.All(cv.ensure_list, [ip_network]), + vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, + default=DEFAULT_LOGIN_ATTEMPT_THRESHOLD): cv.positive_int, + vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: HTTP_SCHEMA, +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the HTTP API and debug interface.""" + conf = config.get(DOMAIN) + + if conf is None: + conf = HTTP_SCHEMA({}) + + api_password = conf[CONF_API_PASSWORD] + server_host = conf[CONF_SERVER_HOST] + server_port = conf[CONF_SERVER_PORT] + development = conf[CONF_DEVELOPMENT] == '1' + ssl_certificate = conf[CONF_SSL_CERTIFICATE] + ssl_key = conf[CONF_SSL_KEY] + cors_origins = conf[CONF_CORS_ORIGINS] + use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] + trusted_networks = conf[CONF_TRUSTED_NETWORKS] + is_ban_enabled = conf[CONF_IP_BAN_ENABLED] + login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] + + if api_password is not None: + logging.getLogger('aiohttp.access').addFilter( + HideSensitiveDataFilter(api_password)) + + server = HomeAssistantWSGI( + hass, + development=development, + server_host=server_host, + server_port=server_port, + api_password=api_password, + ssl_certificate=ssl_certificate, + ssl_key=ssl_key, + cors_origins=cors_origins, + use_x_forwarded_for=use_x_forwarded_for, + trusted_networks=trusted_networks, + login_threshold=login_threshold, + is_ban_enabled=is_ban_enabled + ) + + @asyncio.coroutine + def stop_server(event): + """Callback to stop the server.""" + yield from server.stop() + + @asyncio.coroutine + def start_server(event): + """Callback to start the server.""" + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) + yield from server.start() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) + + hass.http = server + hass.config.api = rem.API(server_host if server_host != '0.0.0.0' + else get_local_ip(), + api_password, server_port, + ssl_certificate is not None) + + return True + + +class HomeAssistantWSGI(object): + """WSGI server for Home Assistant.""" + + def __init__(self, hass, development, api_password, ssl_certificate, + ssl_key, server_host, server_port, cors_origins, + use_x_forwarded_for, trusted_networks, + login_threshold, is_ban_enabled): + """Initialize the WSGI Home Assistant server.""" + import aiohttp_cors + + middlewares = [auth_middleware, staticresource_middleware] + + if is_ban_enabled: + middlewares.insert(0, ban_middleware) + + self.app = web.Application(middlewares=middlewares, loop=hass.loop) + self.app['hass'] = hass + self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for + self.app[KEY_TRUSTED_NETWORKS] = trusted_networks + self.app[KEY_BANS_ENABLED] = is_ban_enabled + self.app[KEY_LOGIN_THRESHOLD] = login_threshold + self.app[KEY_DEVELOPMENT] = development + + self.hass = hass + self.development = development + self.api_password = api_password + self.ssl_certificate = ssl_certificate + self.ssl_key = ssl_key + self.server_host = server_host + self.server_port = server_port + self._handler = None + self.server = None + + if cors_origins: + self.cors = aiohttp_cors.setup(self.app, defaults={ + host: aiohttp_cors.ResourceOptions( + allow_headers=ALLOWED_CORS_HEADERS, + allow_methods='*', + ) for host in cors_origins + }) + else: + self.cors = None + + def register_view(self, view): + """Register a view with the WSGI server. + + The view argument must be a class that inherits from HomeAssistantView. + It is optional to instantiate it before registering; this method will + handle it either way. + """ + if isinstance(view, type): + # Instantiate the view, if needed + view = view() + + if not hasattr(view, 'url'): + class_name = view.__class__.__name__ + raise AttributeError( + '{0} missing required attribute "url"'.format(class_name) + ) + + if not hasattr(view, 'name'): + class_name = view.__class__.__name__ + raise AttributeError( + '{0} missing required attribute "name"'.format(class_name) + ) + + view.register(self.app.router) + + def register_redirect(self, url, redirect_to): + """Register a redirect with the server. + + If given this must be either a string or callable. In case of a + callable it's called with the url adapter that triggered the match and + the values of the URL as keyword arguments and has to return the target + for the redirect, otherwise it has to be a string with placeholders in + rule syntax. + """ + def redirect(request): + """Redirect to location.""" + raise HTTPMovedPermanently(redirect_to) + + self.app.router.add_route('GET', url, redirect) + + def register_static_path(self, url_root, path, cache_length=31): + """Register a folder to serve as a static path. + + Specify optional cache length of asset in days. + """ + if os.path.isdir(path): + self.app.router.add_static(url_root, path) + return + + filepath = Path(path) + + @asyncio.coroutine + def serve_file(request): + """Serve file from disk.""" + res = yield from GZIP_FILE_SENDER.send(request, filepath) + return res + + # aiohttp supports regex matching for variables. Using that as temp + # to work around cache busting MD5. + # Turns something like /static/dev-panel.html into + # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} + base, ext = url_root.rsplit('.', 1) + base, file = base.rsplit('/', 1) + regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext) + url_pattern = "{}/{{filename:{}}}".format(base, regex) + + self.app.router.add_route('GET', url_pattern, serve_file) + + @asyncio.coroutine + def start(self): + """Start the wsgi server.""" + if self.cors is not None: + for route in list(self.app.router.routes()): + self.cors.add(route) + + if self.ssl_certificate: + context = ssl.SSLContext(SSL_VERSION) + context.options |= SSL_OPTS + context.set_ciphers(CIPHERS) + context.load_cert_chain(self.ssl_certificate, self.ssl_key) + else: + context = None + + self._handler = self.app.make_handler() + self.server = yield from self.hass.loop.create_server( + self._handler, self.server_host, self.server_port, ssl=context) + + @asyncio.coroutine + def stop(self): + """Stop the wsgi server.""" + self.server.close() + yield from self.server.wait_closed() + yield from self.app.shutdown() + yield from self._handler.finish_connections(60.0) + yield from self.app.cleanup() + + +class HomeAssistantView(object): + """Base view for all views.""" + + url = None + extra_urls = [] + requires_auth = True # Views inheriting from this class can override this + + # pylint: disable=no-self-use + def json(self, result, status_code=200): + """Return a JSON response.""" + msg = json.dumps( + result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') + return web.Response( + body=msg, content_type=CONTENT_TYPE_JSON, status=status_code) + + def json_message(self, error, status_code=200): + """Return a JSON message response.""" + return self.json({'message': error}, status_code) + + @asyncio.coroutine + # pylint: disable=no-self-use + def file(self, request, fil): + """Return a file.""" + assert isinstance(fil, str), 'only string paths allowed' + response = yield from GZIP_FILE_SENDER.send(request, Path(fil)) + return response + + def register(self, router): + """Register the view with a router.""" + assert self.url is not None, 'No url set for view' + urls = [self.url] + self.extra_urls + + for method in ('get', 'post', 'delete', 'put'): + handler = getattr(self, method, None) + + if not handler: + continue + + handler = request_handler_factory(self, handler) + + for url in urls: + router.add_route(method, url, handler) + + # aiohttp_cors does not work with class based views + # self.app.router.add_route('*', self.url, self, name=self.name) + + # for url in self.extra_urls: + # self.app.router.add_route('*', url, self) + + +def request_handler_factory(view, handler): + """Factory to wrap our handler classes.""" + assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \ + "Handler should be a coroutine or a callback." + + @asyncio.coroutine + def handle(request): + """Handle incoming request.""" + if not request.app['hass'].is_running: + return web.Response(status=503) + + remote_addr = get_real_ip(request) + authenticated = request.get(KEY_AUTHENTICATED, False) + + if view.requires_auth and not authenticated: + yield from process_wrong_login(request) + _LOGGER.warning('Login attempt or request with an invalid ' + 'password from %s', remote_addr) + persistent_notification.async_create( + request.app['hass'], + 'Invalid password used from {}'.format(remote_addr), + 'Login attempt failed', NOTIFICATION_ID_LOGIN) + raise HTTPUnauthorized() + + _LOGGER.info('Serving %s to %s (auth: %s)', + request.path, remote_addr, authenticated) + + result = handler(request, **request.match_info) + + if asyncio.iscoroutine(result): + result = yield from result + + if isinstance(result, web.StreamResponse): + # The method handler returned a ready-made Response, how nice of it + return result + + status_code = 200 + + if isinstance(result, tuple): + result, status_code = result + + if isinstance(result, str): + result = result.encode('utf-8') + elif result is None: + result = b'' + elif not isinstance(result, bytes): + assert False, ('Result should be None, string, bytes or Response. ' + 'Got: {}').format(result) + + return web.Response(body=result, status=status_code) + + return handle diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py new file mode 100644 index 00000000000..14b442e5dde --- /dev/null +++ b/homeassistant/components/http/auth.py @@ -0,0 +1,61 @@ +"""Authentication for HTTP component.""" +import asyncio +import hmac +import logging + +from homeassistant.const import HTTP_HEADER_HA_AUTH +from .util import get_real_ip +from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED + +DATA_API_PASSWORD = 'api_password' + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def auth_middleware(app, handler): + """Authentication middleware.""" + # If no password set, just always set authenticated=True + if app['hass'].http.api_password is None: + @asyncio.coroutine + def no_auth_middleware_handler(request): + """Auth middleware to approve all requests.""" + request[KEY_AUTHENTICATED] = True + return handler(request) + + return no_auth_middleware_handler + + @asyncio.coroutine + def auth_middleware_handler(request): + """Auth middleware to check authentication.""" + hass = app['hass'] + + # Auth code verbose on purpose + authenticated = False + + if hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''), + hass.http.api_password): + # A valid auth header has been set + authenticated = True + + elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''), + hass.http.api_password): + authenticated = True + + elif is_trusted_ip(request): + authenticated = True + + request[KEY_AUTHENTICATED] = authenticated + + return handler(request) + + return auth_middleware_handler + + +def is_trusted_ip(request): + """Test if request is from a trusted ip.""" + ip_addr = get_real_ip(request) + + return ip_addr and any( + ip_addr in trusted_network for trusted_network + in request.app[KEY_TRUSTED_NETWORKS]) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py new file mode 100644 index 00000000000..b3f17c1dd57 --- /dev/null +++ b/homeassistant/components/http/ban.py @@ -0,0 +1,132 @@ +"""Ban logic for HTTP component.""" +import asyncio +from collections import defaultdict +from datetime import datetime +from ipaddress import ip_address +import logging + +from aiohttp.web_exceptions import HTTPForbidden +import voluptuous as vol + +from homeassistant.components import persistent_notification +from homeassistant.config import load_yaml_config_file +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.util.yaml import dump +from .const import ( + KEY_BANS_ENABLED, KEY_BANNED_IPS, KEY_LOGIN_THRESHOLD, + KEY_FAILED_LOGIN_ATTEMPTS) +from .util import get_real_ip + +NOTIFICATION_ID_BAN = 'ip-ban' + +IP_BANS_FILE = 'ip_bans.yaml' +ATTR_BANNED_AT = "banned_at" + +SCHEMA_IP_BAN_ENTRY = vol.Schema({ + vol.Optional('banned_at'): vol.Any(None, cv.datetime) +}) + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def ban_middleware(app, handler): + """IP Ban middleware.""" + if not app[KEY_BANS_ENABLED]: + return handler + + if KEY_BANNED_IPS not in app: + hass = app['hass'] + app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor( + None, load_ip_bans_config, hass.config.path(IP_BANS_FILE)) + + @asyncio.coroutine + def ban_middleware_handler(request): + """Verify if IP is not banned.""" + ip_address_ = get_real_ip(request) + + is_banned = any(ip_ban.ip_address == ip_address_ + for ip_ban in request.app[KEY_BANNED_IPS]) + + if is_banned: + raise HTTPForbidden() + + return handler(request) + + return ban_middleware_handler + + +@asyncio.coroutine +def process_wrong_login(request): + """Process a wrong login attempt.""" + if (not request.app[KEY_BANS_ENABLED] or + request.app[KEY_LOGIN_THRESHOLD] < 1): + return + + if KEY_FAILED_LOGIN_ATTEMPTS not in request.app: + request.app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) + + remote_addr = get_real_ip(request) + + request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 + + if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > + request.app[KEY_LOGIN_THRESHOLD]): + new_ban = IpBan(remote_addr) + request.app[KEY_BANNED_IPS].append(new_ban) + + hass = request.app['hass'] + yield from hass.loop.run_in_executor( + None, update_ip_bans_config, hass.config.path(IP_BANS_FILE), + new_ban) + + _LOGGER.warning('Banned IP %s for too many login attempts', + remote_addr) + + persistent_notification.async_create( + hass, + 'Too many login attempts from {}'.format(remote_addr), + 'Banning IP address', NOTIFICATION_ID_BAN) + + +class IpBan(object): + """Represents banned IP address.""" + + def __init__(self, ip_ban: str, banned_at: datetime=None) -> None: + """Initializing Ip Ban object.""" + self.ip_address = ip_address(ip_ban) + self.banned_at = banned_at or datetime.utcnow() + + +def load_ip_bans_config(path: str): + """Loading list of banned IPs from config file.""" + ip_list = [] + + try: + list_ = load_yaml_config_file(path) + except FileNotFoundError: + return [] + except HomeAssistantError as err: + _LOGGER.error('Unable to load %s: %s', path, str(err)) + return [] + + for ip_ban, ip_info in list_.items(): + try: + ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) + ip_list.append(IpBan(ip_ban, ip_info['banned_at'])) + except vol.Invalid as err: + _LOGGER.error('Failed to load IP ban %s: %s', ip_info, err) + continue + + return ip_list + + +def update_ip_bans_config(path: str, ip_ban: IpBan): + """Update config file with new banned IP address.""" + with open(path, 'a') as out: + ip_ = {str(ip_ban.ip_address): { + ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S") + }} + out.write('\n') + out.write(dump(ip_)) diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py new file mode 100644 index 00000000000..625bc24c461 --- /dev/null +++ b/homeassistant/components/http/const.py @@ -0,0 +1,12 @@ +"""HTTP specific constants.""" +KEY_AUTHENTICATED = 'ha_authenticated' +KEY_USE_X_FORWARDED_FOR = 'ha_use_x_forwarded_for' +KEY_TRUSTED_NETWORKS = 'ha_trusted_networks' +KEY_REAL_IP = 'ha_real_ip' +KEY_BANS_ENABLED = 'ha_bans_enabled' +KEY_BANNED_IPS = 'ha_banned_ips' +KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts' +KEY_LOGIN_THRESHOLD = 'ha_login_treshold' +KEY_DEVELOPMENT = 'ha_development' + +HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For' diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py new file mode 100644 index 00000000000..c8c55870e0f --- /dev/null +++ b/homeassistant/components/http/static.py @@ -0,0 +1,93 @@ +"""Static file handling for HTTP component.""" +import asyncio +import mimetypes +import re + +from aiohttp import hdrs +from aiohttp.file_sender import FileSender +from aiohttp.web_urldispatcher import StaticResource +from aiohttp.web_exceptions import HTTPNotModified + +from .const import KEY_DEVELOPMENT + +_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) + + +class GzipFileSender(FileSender): + """FileSender class capable of sending gzip version if available.""" + + # pylint: disable=invalid-name + + @asyncio.coroutine + def send(self, request, filepath): + """Send filepath to client using request.""" + gzip = False + if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]: + gzip_path = filepath.with_name(filepath.name + '.gz') + + if gzip_path.is_file(): + filepath = gzip_path + gzip = True + + st = filepath.stat() + + modsince = request.if_modified_since + if modsince is not None and st.st_mtime <= modsince.timestamp(): + raise HTTPNotModified() + + ct, encoding = mimetypes.guess_type(str(filepath)) + if not ct: + ct = 'application/octet-stream' + + resp = self._response_factory() + resp.content_type = ct + if encoding: + resp.headers[hdrs.CONTENT_ENCODING] = encoding + if gzip: + resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING + resp.last_modified = st.st_mtime + + # CACHE HACK + if not request.app[KEY_DEVELOPMENT]: + cache_time = 31 * 86400 # = 1 month + resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( + cache_time) + + file_size = st.st_size + + resp.content_length = file_size + with filepath.open('rb') as f: + yield from self._sendfile(request, resp, f, file_size) + + return resp + + +GZIP_FILE_SENDER = GzipFileSender() + + +@asyncio.coroutine +def staticresource_middleware(app, handler): + """Enhance StaticResourceHandler middleware. + + Adds gzip encoding and fingerprinting matching. + """ + inst = getattr(handler, '__self__', None) + if not isinstance(inst, StaticResource): + return handler + + # pylint: disable=protected-access + inst._file_sender = GZIP_FILE_SENDER + + @asyncio.coroutine + def static_middleware_handler(request): + """Strip out fingerprints from resource names.""" + fingerprinted = _FINGERPRINT.match(request.match_info['filename']) + + if fingerprinted: + request.match_info['filename'] = \ + '{}.{}'.format(*fingerprinted.groups()) + + resp = yield from handler(request) + return resp + + return static_middleware_handler diff --git a/homeassistant/components/http/util.py b/homeassistant/components/http/util.py new file mode 100644 index 00000000000..1a5a3d98a22 --- /dev/null +++ b/homeassistant/components/http/util.py @@ -0,0 +1,25 @@ +"""HTTP utilities.""" +from ipaddress import ip_address + +from .const import ( + KEY_REAL_IP, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR) + + +def get_real_ip(request): + """Get IP address of client.""" + if KEY_REAL_IP in request: + return request[KEY_REAL_IP] + + if (request.app[KEY_USE_X_FORWARDED_FOR] and + HTTP_HEADER_X_FORWARDED_FOR in request.headers): + request[KEY_REAL_IP] = ip_address( + request.headers.get(HTTP_HEADER_X_FORWARDED_FOR).split(',')[0]) + else: + peername = request.transport.get_extra_info('peername') + + if peername: + request[KEY_REAL_IP] = ip_address(peername[0]) + else: + request[KEY_REAL_IP] = None + + return request[KEY_REAL_IP] diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index f9b17b552de..d83bffabc91 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -250,11 +250,10 @@ def setup(hass, config): discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - hass.http.register_view(iOSIdentifyDeviceView(hass)) + hass.http.register_view(iOSIdentifyDeviceView) app_config = config.get(DOMAIN, {}) - hass.http.register_view(iOSPushConfigView(hass, - app_config.get(CONF_PUSH, {}))) + hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {}))) return True @@ -266,9 +265,8 @@ class iOSPushConfigView(HomeAssistantView): url = "/api/ios/push" name = "api:ios:push" - def __init__(self, hass, push_config): + def __init__(self, push_config): """Init the view.""" - super().__init__(hass) self.push_config = push_config @callback @@ -283,10 +281,6 @@ class iOSIdentifyDeviceView(HomeAssistantView): url = "/api/ios/identify" name = "api:ios:identify" - def __init__(self, hass): - """Init the view.""" - super().__init__(hass) - @asyncio.coroutine def post(self, request): """Handle the POST request for device identification.""" diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 18e80c4c761..49ab709f8f5 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -101,7 +101,7 @@ def setup(hass, config): message = message.async_render() async_log_entry(hass, name, message, domain, entity_id) - hass.http.register_view(LogbookView(hass, config)) + hass.http.register_view(LogbookView(config)) register_built_in_panel(hass, 'logbook', 'Logbook', 'mdi:format-list-bulleted-type') @@ -118,9 +118,8 @@ class LogbookView(HomeAssistantView): name = 'api:logbook' extra_urls = ['/api/logbook/{datetime}'] - def __init__(self, hass, config): + def __init__(self, config): """Initilalize the logbook view.""" - super().__init__(hass) self.config = config @asyncio.coroutine @@ -146,7 +145,8 @@ class LogbookView(HomeAssistantView): events = recorder.execute(query) return _exclude_events(events, self.config) - events = yield from self.hass.loop.run_in_executor(None, get_results) + events = yield from request.app['hass'].loop.run_in_executor( + None, get_results) return self.json(humanify(events)) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c689cdbccc4..5665699d4f3 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -17,7 +17,7 @@ from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.const import ( @@ -304,7 +304,7 @@ def setup(hass, config): component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.http.register_view(MediaPlayerImageView(hass, component.entities)) + hass.http.register_view(MediaPlayerImageView(component.entities)) component.setup(config) @@ -736,9 +736,8 @@ class MediaPlayerImageView(HomeAssistantView): url = "/api/media_player_proxy/{entity_id}" name = "api:media_player:image" - def __init__(self, hass, entities): + def __init__(self, entities): """Initialize a media player view.""" - super().__init__(hass) self.entities = entities @asyncio.coroutine @@ -748,14 +747,14 @@ class MediaPlayerImageView(HomeAssistantView): if player is None: return web.Response(status=404) - authenticated = (request.authenticated or + authenticated = (request[KEY_AUTHENTICATED] or request.GET.get('token') == player.access_token) if not authenticated: return web.Response(status=401) data, content_type = yield from _async_fetch_image( - self.hass, player.media_image_url) + request.app['hass'], player.media_image_url) if data is None: return web.Response(status=500) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index baf887c1e6e..6621b4be6ab 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -107,8 +107,8 @@ def get_service(hass, config): return None hass.http.register_view( - HTML5PushRegistrationView(hass, registrations, json_path)) - hass.http.register_view(HTML5PushCallbackView(hass, registrations)) + HTML5PushRegistrationView(registrations, json_path)) + hass.http.register_view(HTML5PushCallbackView(registrations)) gcm_api_key = config.get(ATTR_GCM_API_KEY) gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) @@ -168,9 +168,8 @@ class HTML5PushRegistrationView(HomeAssistantView): url = '/api/notify.html5' name = 'api:notify.html5' - def __init__(self, hass, registrations, json_path): + def __init__(self, registrations, json_path): """Init HTML5PushRegistrationView.""" - super().__init__(hass) self.registrations = registrations self.json_path = json_path @@ -237,9 +236,8 @@ class HTML5PushCallbackView(HomeAssistantView): url = '/api/notify.html5/callback' name = 'api:notify.html5/callback' - def __init__(self, hass, registrations): + def __init__(self, registrations): """Init HTML5PushCallbackView.""" - super().__init__(hass) self.registrations = registrations def decode_jwt(self, token): @@ -324,7 +322,7 @@ class HTML5PushCallbackView(HomeAssistantView): event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT, event_payload[ATTR_TYPE]) - self.hass.bus.fire(event_name, event_payload) + request.app['hass'].bus.fire(event_name, event_payload) return self.json({'status': 'ok', 'event': event_payload[ATTR_TYPE]}) diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index cd0346c2469..697fecca077 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -274,7 +274,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.http.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) hass.http.register_view(FitbitAuthCallbackView( - hass, config, add_devices, oauth)) + config, add_devices, oauth)) request_oauth_completion(hass) @@ -286,9 +286,8 @@ class FitbitAuthCallbackView(HomeAssistantView): url = '/auth/fitbit/callback' name = 'auth:fitbit:callback' - def __init__(self, hass, config, add_devices, oauth): + def __init__(self, config, add_devices, oauth): """Initialize the OAuth callback view.""" - super().__init__(hass) self.config = config self.add_devices = add_devices self.oauth = oauth @@ -299,6 +298,7 @@ class FitbitAuthCallbackView(HomeAssistantView): from oauthlib.oauth2.rfc6749.errors import MismatchingStateError from oauthlib.oauth2.rfc6749.errors import MissingTokenError + hass = request.app['hass'] data = request.GET response_message = """Fitbit has been successfully authorized! @@ -306,7 +306,7 @@ class FitbitAuthCallbackView(HomeAssistantView): if data.get('code') is not None: redirect_uri = '{}{}'.format( - self.hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) + hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) try: self.oauth.fetch_access_token(data.get('code'), redirect_uri) @@ -336,12 +336,11 @@ class FitbitAuthCallbackView(HomeAssistantView): ATTR_CLIENT_ID: self.oauth.client_id, ATTR_CLIENT_SECRET: self.oauth.client_secret } - if not config_from_file(self.hass.config.path(FITBIT_CONFIG_FILE), + if not config_from_file(hass.config.path(FITBIT_CONFIG_FILE), config_contents): _LOGGER.error("Failed to save config file") - self.hass.async_add_job(setup_platform, self.hass, self.config, - self.add_devices) + hass.async_add_job(setup_platform, hass, self.config, self.add_devices) return html_response diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index 8c88a4e22d2..7f63ac5b4e6 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = {} hass.http.register_view(TorqueReceiveDataView( - hass, email, vehicle, sensors, add_devices)) + email, vehicle, sensors, add_devices)) return True @@ -69,9 +69,8 @@ class TorqueReceiveDataView(HomeAssistantView): url = API_PATH name = 'api:torque' - def __init__(self, hass, email, vehicle, sensors, add_devices): + def __init__(self, email, vehicle, sensors, add_devices): """Initialize a Torque view.""" - super().__init__(hass) self.email = email self.vehicle = vehicle self.sensors = sensors @@ -80,6 +79,7 @@ class TorqueReceiveDataView(HomeAssistantView): @callback def get(self, request): """Handle Torque data request.""" + hass = request.app['hass'] data = request.GET if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]: @@ -108,7 +108,7 @@ class TorqueReceiveDataView(HomeAssistantView): self.sensors[pid] = TorqueSensor( ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]), units.get(pid, None)) - self.hass.async_add_job(self.add_devices, [self.sensors[pid]]) + hass.async_add_job(self.add_devices, [self.sensors[pid]]) return None diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index 74505cdcdc2..95da15898b9 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -97,6 +97,7 @@ class NetioApiView(HomeAssistantView): @callback def get(self, request, host): """Request handler.""" + hass = request.app['hass'] data = request.GET states, consumptions, cumulated_consumptions, start_dates = \ [], [], [], [] @@ -119,7 +120,7 @@ class NetioApiView(HomeAssistantView): ndev.start_dates = start_dates for dev in DEVICES[host].entities: - self.hass.async_add_job(dev.async_update_ha_state()) + hass.async_add_job(dev.async_update_ha_state()) return self.json(True) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1b0921ccc95..64a4e7e5c45 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -360,7 +360,6 @@ HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' HTTP_HEADER_CACHE_CONTROL = 'Cache-Control' HTTP_HEADER_EXPIRES = 'Expires' HTTP_HEADER_ORIGIN = 'Origin' -HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For' HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With' HTTP_HEADER_ACCEPT = 'Accept' HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py new file mode 100644 index 00000000000..d324e7253b7 --- /dev/null +++ b/homeassistant/util/logging.py @@ -0,0 +1,17 @@ +"""Logging utilities.""" +import logging + + +class HideSensitiveDataFilter(logging.Filter): + """Filter API password calls.""" + + def __init__(self, text): + """Initialize sensitive data filter.""" + super().__init__() + self.text = text + + def filter(self, record): + """Hide sensitive data in messages.""" + record.msg = record.msg.replace(self.text, '*******') + + return True diff --git a/tests/common.py b/tests/common.py index 25a10783c28..fc779e120f8 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,6 +10,8 @@ import logging import threading from contextlib import contextmanager +from aiohttp import web + from homeassistant import core as ha, loader from homeassistant.bootstrap import ( setup_component, async_prepare_setup_component) @@ -22,6 +24,9 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT) from homeassistant.components import sun, mqtt +from homeassistant.components.http.auth import auth_middleware +from homeassistant.components.http.const import ( + KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED) _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) @@ -210,13 +215,23 @@ def mock_http_component(hass): """Store registered view.""" if isinstance(view, type): # Instantiate the view, if needed - view = view(hass) + view = view() hass.http.views[view.name] = view hass.http.register_view = mock_register_view +def mock_http_component_app(hass): + """Create an aiohttp.web.Application instance for testing.""" + hass.http.api_password = None + app = web.Application(middlewares=[auth_middleware], loop=hass.loop) + app['hass'] = hass + app[KEY_USE_X_FORWARDED_FOR] = False + app[KEY_BANS_ENABLED] = False + return app + + def mock_mqtt_component(hass): """Mock the MQTT component.""" with mock.patch('homeassistant.components.mqtt.MQTT') as mock_mqtt: diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index e2ce9c15936..ac7b0063158 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -27,8 +27,8 @@ def test_fetching_url(aioclient_mock, hass, test_client): resp = yield from client.get('/api/camera_proxy/camera.config_test') - assert aioclient_mock.call_count == 1 assert resp.status == 200 + assert aioclient_mock.call_count == 1 body = yield from resp.text() assert body == 'hello world' diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py new file mode 100644 index 00000000000..869e80fff75 --- /dev/null +++ b/tests/components/http/__init__.py @@ -0,0 +1 @@ +"""Tests for the HTTP component.""" diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py new file mode 100644 index 00000000000..d41a1f03d1b --- /dev/null +++ b/tests/components/http/test_auth.py @@ -0,0 +1,169 @@ +"""The tests for the Home Assistant HTTP component.""" +# pylint: disable=protected-access +import logging +from ipaddress import ip_address, ip_network +from unittest.mock import patch + +import requests + +from homeassistant import bootstrap, const +import homeassistant.components.http as http +from homeassistant.components.http.const import ( + KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR) + +from tests.common import get_test_instance_port, get_test_home_assistant + +API_PASSWORD = 'test1234' +SERVER_PORT = get_test_instance_port() +HTTP_BASE = '127.0.0.1:{}'.format(SERVER_PORT) +HTTP_BASE_URL = 'http://{}'.format(HTTP_BASE) +HA_HEADERS = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} +# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases +TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1', + 'FD01:DB8::1'] +TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', + '2001:DB8:ABCD::1'] +UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1'] + +hass = None + + +def _url(path=''): + """Helper method to generate URLs.""" + return HTTP_BASE_URL + path + + +# pylint: disable=invalid-name +def setUpModule(): + """Initialize a Home Assistant server.""" + global hass + + hass = get_test_home_assistant() + + bootstrap.setup_component( + hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: SERVER_PORT, + } + } + ) + + bootstrap.setup_component(hass, 'api') + + hass.http.app[KEY_TRUSTED_NETWORKS] = [ + ip_network(trusted_network) + for trusted_network in TRUSTED_NETWORKS] + + hass.start() + + +# pylint: disable=invalid-name +def tearDownModule(): + """Stop the Home Assistant server.""" + hass.stop() + + +class TestHttp: + """Test HTTP component.""" + + def test_access_denied_without_password(self): + """Test access without password.""" + req = requests.get(_url(const.URL_API)) + + assert req.status_code == 401 + + def test_access_denied_with_wrong_password_in_header(self): + """Test access with wrong password.""" + req = requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) + + assert req.status_code == 401 + + def test_access_denied_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in UNTRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 401, \ + "{} shouldn't be trusted".format(remote_addr) + + def test_access_denied_with_untrusted_ip(self, caplog): + """Test access with an untrusted ip address.""" + for remote_addr in UNTRUSTED_ADDRESSES: + with patch('homeassistant.components.http.' + 'util.get_real_ip', + return_value=ip_address(remote_addr)): + req = requests.get( + _url(const.URL_API), params={'api_password': ''}) + + assert req.status_code == 401, \ + "{} shouldn't be trusted".format(remote_addr) + + def test_access_with_password_in_header(self, caplog): + """Test access with password in URL.""" + # Hide logging from requests package that we use to test logging + caplog.set_level( + logging.WARNING, logger='requests.packages.urllib3.connectionpool') + + req = requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD}) + + assert req.status_code == 200 + + logs = caplog.text + + assert const.URL_API in logs + assert API_PASSWORD not in logs + + def test_access_denied_with_wrong_password_in_url(self): + """Test access with wrong password.""" + req = requests.get( + _url(const.URL_API), params={'api_password': 'wrongpassword'}) + + assert req.status_code == 401 + + def test_access_with_password_in_url(self, caplog): + """Test access with password in URL.""" + # Hide logging from requests package that we use to test logging + caplog.set_level( + logging.WARNING, logger='requests.packages.urllib3.connectionpool') + + req = requests.get( + _url(const.URL_API), params={'api_password': API_PASSWORD}) + + assert req.status_code == 200 + + logs = caplog.text + + assert const.URL_API in logs + assert API_PASSWORD not in logs + + def test_access_granted_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.app[KEY_USE_X_FORWARDED_FOR] = True + for remote_addr in TRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 200, \ + "{} should be trusted".format(remote_addr) + + def test_access_granted_with_trusted_ip(self, caplog): + """Test access with trusted addresses.""" + for remote_addr in TRUSTED_ADDRESSES: + with patch('homeassistant.components.http.' + 'auth.get_real_ip', + return_value=ip_address(remote_addr)): + req = requests.get( + _url(const.URL_API), params={'api_password': ''}) + + assert req.status_code == 200, \ + '{} should be trusted'.format(remote_addr) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py new file mode 100644 index 00000000000..b2aeca2917f --- /dev/null +++ b/tests/components/http/test_ban.py @@ -0,0 +1,118 @@ +"""The tests for the Home Assistant HTTP component.""" +# pylint: disable=protected-access +from ipaddress import ip_address +from unittest.mock import patch, mock_open + +import requests + +from homeassistant import bootstrap, const +import homeassistant.components.http as http +from homeassistant.components.http.const import ( + KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD, KEY_BANNED_IPS) +from homeassistant.components.http.ban import IpBan, IP_BANS_FILE + +from tests.common import get_test_instance_port, get_test_home_assistant + +API_PASSWORD = 'test1234' +SERVER_PORT = get_test_instance_port() +HTTP_BASE = '127.0.0.1:{}'.format(SERVER_PORT) +HTTP_BASE_URL = 'http://{}'.format(HTTP_BASE) +HA_HEADERS = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} +BANNED_IPS = ['200.201.202.203', '100.64.0.2'] + +hass = None + + +def _url(path=''): + """Helper method to generate URLs.""" + return HTTP_BASE_URL + path + + +# pylint: disable=invalid-name +def setUpModule(): + """Initialize a Home Assistant server.""" + global hass + + hass = get_test_home_assistant() + + bootstrap.setup_component( + hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: SERVER_PORT, + } + } + ) + + bootstrap.setup_component(hass, 'api') + + hass.http.app[KEY_BANNED_IPS] = [IpBan(banned_ip) for banned_ip + in BANNED_IPS] + hass.start() + + +# pylint: disable=invalid-name +def tearDownModule(): + """Stop the Home Assistant server.""" + hass.stop() + + +class TestHttp: + """Test HTTP component.""" + + def test_access_from_banned_ip(self): + """Test accessing to server from banned IP. Both trusted and not.""" + hass.http.app[KEY_BANS_ENABLED] = True + for remote_addr in BANNED_IPS: + with patch('homeassistant.components.http.' + 'ban.get_real_ip', + return_value=ip_address(remote_addr)): + req = requests.get( + _url(const.URL_API)) + assert req.status_code == 403 + + def test_access_from_banned_ip_when_ban_is_off(self): + """Test accessing to server from banned IP when feature is off""" + hass.http.app[KEY_BANS_ENABLED] = False + for remote_addr in BANNED_IPS: + with patch('homeassistant.components.http.' + 'ban.get_real_ip', + return_value=ip_address(remote_addr)): + req = requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status_code == 200 + + def test_ip_bans_file_creation(self): + """Testing if banned IP file created""" + hass.http.app[KEY_BANS_ENABLED] = True + hass.http.app[KEY_LOGIN_THRESHOLD] = 1 + + m = mock_open() + + def call_server(): + with patch('homeassistant.components.http.' + 'ban.get_real_ip', + return_value=ip_address("200.201.202.204")): + print("GETTING API") + return requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: 'Wrong password'}) + + with patch('homeassistant.components.http.ban.open', m, create=True): + req = call_server() + assert req.status_code == 401 + assert len(hass.http.app[KEY_BANNED_IPS]) == len(BANNED_IPS) + assert m.call_count == 0 + + req = call_server() + assert req.status_code == 401 + assert len(hass.http.app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1 + m.assert_called_once_with(hass.config.path(IP_BANS_FILE), 'a') + + req = call_server() + assert req.status_code == 403 + assert m.call_count == 1 diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py new file mode 100644 index 00000000000..a1e0532bc14 --- /dev/null +++ b/tests/components/http/test_init.py @@ -0,0 +1,111 @@ +"""The tests for the Home Assistant HTTP component.""" +import requests + +from homeassistant import bootstrap, const +import homeassistant.components.http as http + +from tests.common import get_test_instance_port, get_test_home_assistant + +API_PASSWORD = 'test1234' +SERVER_PORT = get_test_instance_port() +HTTP_BASE = '127.0.0.1:{}'.format(SERVER_PORT) +HTTP_BASE_URL = 'http://{}'.format(HTTP_BASE) +HA_HEADERS = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} +CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE] + +hass = None + + +def _url(path=''): + """Helper method to generate URLs.""" + return HTTP_BASE_URL + path + + +# pylint: disable=invalid-name +def setUpModule(): + """Initialize a Home Assistant server.""" + global hass + + hass = get_test_home_assistant() + + bootstrap.setup_component( + hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: SERVER_PORT, + http.CONF_CORS_ORIGINS: CORS_ORIGINS, + } + } + ) + + bootstrap.setup_component(hass, 'api') + + hass.start() + + +# pylint: disable=invalid-name +def tearDownModule(): + """Stop the Home Assistant server.""" + hass.stop() + + +class TestHttp: + """Test HTTP component.""" + + def test_cors_allowed_with_password_in_url(self): + """Test cross origin resource sharing with password in url.""" + req = requests.get(_url(const.URL_API), + params={'api_password': API_PASSWORD}, + headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL}) + + allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN + + assert req.status_code == 200 + assert req.headers.get(allow_origin) == HTTP_BASE_URL + + def test_cors_allowed_with_password_in_header(self): + """Test cross origin resource sharing with password in header.""" + headers = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD, + const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL + } + req = requests.get(_url(const.URL_API), headers=headers) + + allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN + + assert req.status_code == 200 + assert req.headers.get(allow_origin) == HTTP_BASE_URL + + def test_cors_denied_without_origin_header(self): + """Test cross origin resource sharing with password in header.""" + headers = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD + } + req = requests.get(_url(const.URL_API), headers=headers) + + allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN + allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS + + assert req.status_code == 200 + assert allow_origin not in req.headers + assert allow_headers not in req.headers + + def test_cors_preflight_allowed(self): + """Test cross origin resource sharing preflight (OPTIONS) request.""" + headers = { + const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL, + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'x-ha-access' + } + req = requests.options(_url(const.URL_API), headers=headers) + + allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN + allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS + + assert req.status_code == 200 + assert req.headers.get(allow_origin) == HTTP_BASE_URL + assert req.headers.get(allow_headers) == \ + const.HTTP_HEADER_HA_AUTH.upper() diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 82e43300db7..8d27a11e094 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -3,10 +3,10 @@ import asyncio import json from unittest.mock import patch, MagicMock, mock_open -from aiohttp import web - from homeassistant.components.notify import html5 +from tests.common import mock_http_component_app + SUBSCRIPTION_1 = { 'browser': 'chrome', 'subscription': { @@ -121,7 +121,8 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == {} - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False @@ -153,7 +154,8 @@ class TestHtml5Notify(object): view = hass.mock_calls[1][1][0] - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False @@ -208,7 +210,8 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == config - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False @@ -253,7 +256,8 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == config - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False @@ -296,7 +300,8 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == config - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False @@ -331,7 +336,8 @@ class TestHtml5Notify(object): view = hass.mock_calls[2][1][0] - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False @@ -387,7 +393,8 @@ class TestHtml5Notify(object): bearer_token = "Bearer {}".format(push_payload['data']['jwt']) - app = web.Application(loop=loop) + hass.loop = loop + app = mock_http_component_app(hass) view.register(app.router) client = yield from test_client(app) hass.http.is_banned_ip.return_value = False diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 3ff366babd9..a56fac9ed5d 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -6,7 +6,7 @@ import unittest import requests import homeassistant.bootstrap as bootstrap -from homeassistant.components import frontend, http +from homeassistant.components import http from homeassistant.const import HTTP_HEADER_HA_AUTH from tests.common import get_test_instance_port, get_test_home_assistant @@ -45,7 +45,6 @@ def setUpModule(): def tearDownModule(): """Stop everything that was started.""" hass.stop() - frontend.PANELS = {} class TestFrontend(unittest.TestCase): diff --git a/tests/components/test_http.py b/tests/components/test_http.py deleted file mode 100644 index 83cda160ac1..00000000000 --- a/tests/components/test_http.py +++ /dev/null @@ -1,285 +0,0 @@ -"""The tests for the Home Assistant HTTP component.""" -# pylint: disable=protected-access -import logging -from ipaddress import ip_network -from unittest.mock import patch, mock_open - -import requests - -from homeassistant import bootstrap, const -import homeassistant.components.http as http - -from tests.common import get_test_instance_port, get_test_home_assistant - -API_PASSWORD = 'test1234' -SERVER_PORT = get_test_instance_port() -HTTP_BASE = '127.0.0.1:{}'.format(SERVER_PORT) -HTTP_BASE_URL = 'http://{}'.format(HTTP_BASE) -HA_HEADERS = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD, - const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, -} -# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases -TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1', - 'FD01:DB8::1'] -TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', - '2001:DB8:ABCD::1'] -UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1'] -BANNED_IPS = ['200.201.202.203', '100.64.0.1'] - -CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE] - -hass = None - - -def _url(path=''): - """Helper method to generate URLs.""" - return HTTP_BASE_URL + path - - -# pylint: disable=invalid-name -def setUpModule(): - """Initialize a Home Assistant server.""" - global hass - - hass = get_test_home_assistant() - - hass.bus.listen('test_event', lambda _: _) - hass.states.set('test.test', 'a_state') - - bootstrap.setup_component( - hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_API_PASSWORD: API_PASSWORD, - http.CONF_SERVER_PORT: SERVER_PORT, - http.CONF_CORS_ORIGINS: CORS_ORIGINS, - } - } - ) - - bootstrap.setup_component(hass, 'api') - - hass.http.trusted_networks = [ - ip_network(trusted_network) - for trusted_network in TRUSTED_NETWORKS] - - hass.http.ip_bans = [http.IpBan(banned_ip) - for banned_ip in BANNED_IPS] - - hass.start() - - -# pylint: disable=invalid-name -def tearDownModule(): - """Stop the Home Assistant server.""" - hass.stop() - - -class TestHttp: - """Test HTTP component.""" - - def test_access_denied_without_password(self): - """Test access without password.""" - req = requests.get(_url(const.URL_API)) - - assert req.status_code == 401 - - def test_access_denied_with_wrong_password_in_header(self): - """Test access with wrong password.""" - req = requests.get( - _url(const.URL_API), - headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) - - assert req.status_code == 401 - - def test_access_denied_with_x_forwarded_for(self, caplog): - """Test access denied through the X-Forwarded-For http header.""" - hass.http.use_x_forwarded_for = True - for remote_addr in UNTRUSTED_ADDRESSES: - req = requests.get(_url(const.URL_API), headers={ - const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) - - assert req.status_code == 401, \ - "{} shouldn't be trusted".format(remote_addr) - - def test_access_denied_with_untrusted_ip(self, caplog): - """Test access with an untrusted ip address.""" - for remote_addr in UNTRUSTED_ADDRESSES: - with patch('homeassistant.components.http.' - 'HomeAssistantWSGI.get_real_ip', - return_value=remote_addr): - req = requests.get( - _url(const.URL_API), params={'api_password': ''}) - - assert req.status_code == 401, \ - "{} shouldn't be trusted".format(remote_addr) - - def test_access_with_password_in_header(self, caplog): - """Test access with password in URL.""" - # Hide logging from requests package that we use to test logging - caplog.set_level( - logging.WARNING, logger='requests.packages.urllib3.connectionpool') - - req = requests.get( - _url(const.URL_API), - headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD}) - - assert req.status_code == 200 - - logs = caplog.text - - # assert const.URL_API in logs - assert API_PASSWORD not in logs - - def test_access_denied_with_wrong_password_in_url(self): - """Test access with wrong password.""" - req = requests.get( - _url(const.URL_API), params={'api_password': 'wrongpassword'}) - - assert req.status_code == 401 - - def test_access_with_password_in_url(self, caplog): - """Test access with password in URL.""" - # Hide logging from requests package that we use to test logging - caplog.set_level( - logging.WARNING, logger='requests.packages.urllib3.connectionpool') - - req = requests.get( - _url(const.URL_API), params={'api_password': API_PASSWORD}) - - assert req.status_code == 200 - - logs = caplog.text - - # assert const.URL_API in logs - assert API_PASSWORD not in logs - - def test_access_granted_with_x_forwarded_for(self, caplog): - """Test access denied through the X-Forwarded-For http header.""" - hass.http.use_x_forwarded_for = True - for remote_addr in TRUSTED_ADDRESSES: - req = requests.get(_url(const.URL_API), headers={ - const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) - - assert req.status_code == 200, \ - "{} should be trusted".format(remote_addr) - - def test_access_granted_with_trusted_ip(self, caplog): - """Test access with trusted addresses.""" - for remote_addr in TRUSTED_ADDRESSES: - with patch('homeassistant.components.http.' - 'HomeAssistantWSGI.get_real_ip', - return_value=remote_addr): - req = requests.get( - _url(const.URL_API), params={'api_password': ''}) - - assert req.status_code == 200, \ - '{} should be trusted'.format(remote_addr) - - def test_cors_allowed_with_password_in_url(self): - """Test cross origin resource sharing with password in url.""" - req = requests.get(_url(const.URL_API), - params={'api_password': API_PASSWORD}, - headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL}) - - allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN - - assert req.status_code == 200 - assert req.headers.get(allow_origin) == HTTP_BASE_URL - - def test_cors_allowed_with_password_in_header(self): - """Test cross origin resource sharing with password in header.""" - headers = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD, - const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL - } - req = requests.get(_url(const.URL_API), headers=headers) - - allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN - - assert req.status_code == 200 - assert req.headers.get(allow_origin) == HTTP_BASE_URL - - def test_cors_denied_without_origin_header(self): - """Test cross origin resource sharing with password in header.""" - headers = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD - } - req = requests.get(_url(const.URL_API), headers=headers) - - allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN - allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS - - assert req.status_code == 200 - assert allow_origin not in req.headers - assert allow_headers not in req.headers - - def test_cors_preflight_allowed(self): - """Test cross origin resource sharing preflight (OPTIONS) request.""" - headers = { - const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL, - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'x-ha-access' - } - req = requests.options(_url(const.URL_API), headers=headers) - - allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN - allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS - - assert req.status_code == 200 - assert req.headers.get(allow_origin) == HTTP_BASE_URL - assert req.headers.get(allow_headers) == \ - const.HTTP_HEADER_HA_AUTH.upper() - - def test_access_from_banned_ip(self): - """Test accessing to server from banned IP. Both trusted and not.""" - hass.http.is_ban_enabled = True - for remote_addr in BANNED_IPS: - with patch('homeassistant.components.http.' - 'HomeAssistantWSGI.get_real_ip', - return_value=remote_addr): - req = requests.get( - _url(const.URL_API)) - assert req.status_code == 403 - - def test_access_from_banned_ip_when_ban_is_off(self): - """Test accessing to server from banned IP when feature is off""" - hass.http.is_ban_enabled = False - for remote_addr in BANNED_IPS: - with patch('homeassistant.components.http.' - 'HomeAssistantWSGI.get_real_ip', - return_value=remote_addr): - req = requests.get( - _url(const.URL_API), - headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status_code == 200 - - def test_ip_bans_file_creation(self): - """Testing if banned IP file created""" - hass.http.is_ban_enabled = True - hass.http.login_threshold = 1 - - m = mock_open() - - def call_server(): - with patch('homeassistant.components.http.' - 'HomeAssistantWSGI.get_real_ip', - return_value="200.201.202.204"): - return requests.get( - _url(const.URL_API), - headers={const.HTTP_HEADER_HA_AUTH: 'Wrong password'}) - - with patch('homeassistant.components.http.open', m, create=True): - req = call_server() - assert req.status_code == 401 - assert len(hass.http.ip_bans) == len(BANNED_IPS) - assert m.call_count == 0 - - req = call_server() - assert req.status_code == 401 - assert len(hass.http.ip_bans) == len(BANNED_IPS) + 1 - m.assert_called_once_with(hass.config.path(http.IP_BANS), 'a') - - req = call_server() - assert req.status_code == 403 - assert m.call_count == 1 diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index e709d4693c7..b4994c5f136 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -165,7 +165,15 @@ class TestCheckConfig(unittest.TestCase): self.assertDictEqual({ 'components': {'http': {'api_password': 'abc123', + 'cors_allowed_origins': [], + 'development': '0', + 'ip_ban_enabled': True, + 'login_attempts_threshold': -1, + 'server_host': '0.0.0.0', 'server_port': 8123, + 'ssl_certificate': None, + 'ssl_key': None, + 'trusted_networks': [], 'use_x_forwarded_for': False}}, 'except': {}, 'secret_cache': {secrets_path: {'http_pw': 'abc123'}}, From 03e0c7c71cbc912d15543c417223c935b14a74d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Nov 2016 10:10:29 -0800 Subject: [PATCH 061/137] Prevent edimax from doing I/O in event loop (#4584) --- homeassistant/components/switch/edimax.py | 34 ++++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 41746f9a0ef..0e2183920f3 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -49,6 +49,9 @@ class SmartPlugSwitch(SwitchDevice): """Initialize the switch.""" self.smartplug = smartplug self._name = name + self._now_power = None + self._now_energy_day = None + self._state = False @property def name(self): @@ -58,27 +61,17 @@ class SmartPlugSwitch(SwitchDevice): @property def current_power_mwh(self): """Return the current power usage in mWh.""" - try: - return float(self.smartplug.now_power) / 1000000.0 - except ValueError: - return None - except TypeError: - return None + return self._now_power @property def today_power_mw(self): """Return the today total power usage in mW.""" - try: - return float(self.smartplug.now_energy_day) / 1000.0 - except ValueError: - return None - except TypeError: - return None + return self._now_energy_day @property def is_on(self): """Return true if switch is on.""" - return self.smartplug.state == 'ON' + return self._state def turn_on(self, **kwargs): """Turn the switch on.""" @@ -87,3 +80,18 @@ class SmartPlugSwitch(SwitchDevice): def turn_off(self): """Turn the switch off.""" self.smartplug.state = 'OFF' + + def update(self): + """Update edimax switch.""" + try: + self._now_power = float(self.smartplug.now_power) / 1000000.0 + except (TypeError, ValueError): + self._now_power = None + + try: + self._now_energy_day = (float(self.smartplug.now_energy_day) / + 1000.0) + except (TypeError, ValueError): + self._now_energy_day = None + + self._state = self.smartplug.state == 'ON' From 914a868fbdb380508ec1c32e6339071fa86014e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Nov 2016 18:23:28 -0800 Subject: [PATCH 062/137] Add websocket API (#4582) * Add websocket API * Add identifiers to interactions * Allow unsubscribing event listeners * Add support for fetching data * Clean up handling code websockets api * Lint * Add Home Assistant version to auth messages * Py.test be less verbose in tox --- .../frontend/www_static/websocket_test.html | 125 ++++++ homeassistant/components/http/auth.py | 17 +- homeassistant/components/websocket_api.py | 401 ++++++++++++++++++ requirements_test.txt | 1 + tests/common.py | 14 +- tests/components/test_websocket_api.py | 285 +++++++++++++ tox.ini | 2 +- 7 files changed, 831 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/frontend/www_static/websocket_test.html create mode 100644 homeassistant/components/websocket_api.py create mode 100644 tests/components/test_websocket_api.py diff --git a/homeassistant/components/frontend/www_static/websocket_test.html b/homeassistant/components/frontend/www_static/websocket_test.html new file mode 100644 index 00000000000..d4c0974899c --- /dev/null +++ b/homeassistant/components/frontend/www_static/websocket_test.html @@ -0,0 +1,125 @@ + + + + + WebSocket debug + + + +
+ +
+Examples:
+{
+  "id": 2, "type": "subscribe_events", "event_type": "state_changed"
+}
+
+{
+  "id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
+}
+
+{
+  "id": 4, "type": "unsubscribe_events", "subscription": 2
+}
+
+{
+  "id": 5, "type": "get_states"
+}
+
+{
+  "id": 6, "type": "get_config"
+}
+
+{
+  "id": 7, "type": "get_services"
+}
+
+{
+  "id": 8, "type": "get_panels"
+}
+      
+
+
+ + + +
+ +

+
+    
+    
+  
+
diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py
index 14b442e5dde..6ff653eef35 100644
--- a/homeassistant/components/http/auth.py
+++ b/homeassistant/components/http/auth.py
@@ -28,18 +28,17 @@ def auth_middleware(app, handler):
     @asyncio.coroutine
     def auth_middleware_handler(request):
         """Auth middleware to check authentication."""
-        hass = app['hass']
-
         # Auth code verbose on purpose
         authenticated = False
 
-        if hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
-                               hass.http.api_password):
+        if (HTTP_HEADER_HA_AUTH in request.headers and
+                validate_password(request,
+                                  request.headers[HTTP_HEADER_HA_AUTH])):
             # A valid auth header has been set
             authenticated = True
 
-        elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
-                                 hass.http.api_password):
+        elif (DATA_API_PASSWORD in request.GET and
+              validate_password(request, request.GET[DATA_API_PASSWORD])):
             authenticated = True
 
         elif is_trusted_ip(request):
@@ -59,3 +58,9 @@ def is_trusted_ip(request):
     return ip_addr and any(
         ip_addr in trusted_network for trusted_network
         in request.app[KEY_TRUSTED_NETWORKS])
+
+
+def validate_password(request, api_password):
+    """Test if password is valid."""
+    return hmac.compare_digest(api_password,
+                               request.app['hass'].http.api_password)
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
new file mode 100644
index 00000000000..391c27e8894
--- /dev/null
+++ b/homeassistant/components/websocket_api.py
@@ -0,0 +1,401 @@
+"""Websocket based API for Home Assistant."""
+import asyncio
+from functools import partial
+import json
+import logging
+
+from aiohttp import web
+import voluptuous as vol
+from voluptuous.humanize import humanize_error
+
+from homeassistant.const import (
+    MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP,
+    __version__)
+from homeassistant.components import api, frontend
+from homeassistant.core import callback
+from homeassistant.remote import JSONEncoder
+from homeassistant.helpers import config_validation as cv
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.auth import validate_password
+from homeassistant.components.http.const import KEY_AUTHENTICATED
+
+DOMAIN = 'websocket_api'
+
+URL = "/api/websocket"
+DEPENDENCIES = 'http',
+
+ERR_ID_REUSE = 1
+ERR_INVALID_FORMAT = 2
+ERR_NOT_FOUND = 3
+
+TYPE_AUTH = 'auth'
+TYPE_AUTH_OK = 'auth_ok'
+TYPE_AUTH_REQUIRED = 'auth_required'
+TYPE_AUTH_INVALID = 'auth_invalid'
+TYPE_EVENT = 'event'
+TYPE_SUBSCRIBE_EVENTS = 'subscribe_events'
+TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events'
+TYPE_CALL_SERVICE = 'call_service'
+TYPE_GET_STATES = 'get_states'
+TYPE_GET_SERVICES = 'get_services'
+TYPE_GET_CONFIG = 'get_config'
+TYPE_GET_PANELS = 'get_panels'
+TYPE_RESULT = 'result'
+
+_LOGGER = logging.getLogger(__name__)
+
+JSON_DUMP = partial(json.dumps, cls=JSONEncoder)
+
+AUTH_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('type'): TYPE_AUTH,
+    vol.Required('api_password'): str,
+})
+
+SUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_SUBSCRIBE_EVENTS,
+    vol.Optional('event_type', default=MATCH_ALL): str,
+})
+
+UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS,
+    vol.Required('subscription'): cv.positive_int,
+})
+
+CALL_SERVICE_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_CALL_SERVICE,
+    vol.Required('domain'): str,
+    vol.Required('service'): str,
+    vol.Optional('service_data', default=None): dict
+})
+
+GET_STATES_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_STATES,
+})
+
+GET_SERVICES_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_SERVICES,
+})
+
+GET_CONFIG_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_CONFIG,
+})
+
+GET_PANELS_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_PANELS,
+})
+
+BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): vol.Any(TYPE_CALL_SERVICE,
+                                  TYPE_SUBSCRIBE_EVENTS,
+                                  TYPE_UNSUBSCRIBE_EVENTS,
+                                  TYPE_GET_STATES,
+                                  TYPE_GET_SERVICES,
+                                  TYPE_GET_CONFIG,
+                                  TYPE_GET_PANELS)
+}, extra=vol.ALLOW_EXTRA)
+
+
+def auth_ok_message():
+    """Return an auth_ok message."""
+    return {
+        'type': TYPE_AUTH_OK,
+        'ha_version': __version__,
+    }
+
+
+def auth_required_message():
+    """Return an auth_required message."""
+    return {
+        'type': TYPE_AUTH_REQUIRED,
+        'ha_version': __version__,
+    }
+
+
+def auth_invalid_message(message):
+    """Return an auth_invalid message."""
+    return {
+        'type': TYPE_AUTH_INVALID,
+        'message': message,
+    }
+
+
+def event_message(iden, event):
+    """Return an event message."""
+    return {
+        'id': iden,
+        'type': TYPE_EVENT,
+        'event': event.as_dict(),
+    }
+
+
+def error_message(iden, code, message):
+    """Return an error result message."""
+    return {
+        'id': iden,
+        'type': TYPE_RESULT,
+        'success': False,
+        'error': {
+            'code': code,
+            'message': message,
+        },
+    }
+
+
+def result_message(iden, result=None):
+    """Return a success result message."""
+    return {
+        'id': iden,
+        'type': TYPE_RESULT,
+        'success': True,
+        'result': result,
+    }
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+    """Initialize the websocket API."""
+    hass.http.register_view(WebsocketAPIView)
+    return True
+
+
+class WebsocketAPIView(HomeAssistantView):
+    """View to serve a websockets endpoint."""
+
+    name = "websocketapi"
+    url = URL
+    requires_auth = False
+
+    @asyncio.coroutine
+    def get(self, request):
+        """Handle an incoming websocket connection."""
+        # pylint: disable=no-self-use
+        return ActiveConnection(request.app['hass'], request).handle()
+
+
+class ActiveConnection:
+    """Handle an active websocket client connection."""
+
+    def __init__(self, hass, request):
+        """Initialize an active connection."""
+        self.hass = hass
+        self.request = request
+        self.wsock = None
+        self.socket_task = None
+        self.event_listeners = {}
+
+    def debug(self, message1, message2=''):
+        """Print a debug message."""
+        _LOGGER.debug('WS %s: %s %s', id(self.wsock), message1, message2)
+
+    def log_error(self, message1, message2=''):
+        """Print an error message."""
+        _LOGGER.error('WS %s: %s %s', id(self.wsock), message1, message2)
+
+    def send_message(self, message):
+        """Helper method to send messages."""
+        self.debug('Sending', message)
+        self.wsock.send_json(message, dumps=JSON_DUMP)
+
+    @callback
+    def _cancel_connection(self, event):
+        """Cancel this connection."""
+        self.socket_task.cancel()
+
+    @asyncio.coroutine
+    def _call_service_helper(self, msg):
+        """Helper to call a service and fire complete message."""
+        yield from self.hass.services.async_call(msg['domain'], msg['service'],
+                                                 msg['service_data'], True)
+        try:
+            self.send_message(result_message(msg['id']))
+        except RuntimeError:
+            # Socket has been closed.
+            pass
+
+    @callback
+    def _forward_event(self, iden, event):
+        """Helper to forward events to websocket."""
+        if event.event_type == EVENT_TIME_CHANGED:
+            return
+
+        try:
+            self.send_message(event_message(iden, event))
+        except RuntimeError:
+            # Socket has been closed.
+            pass
+
+    @asyncio.coroutine
+    def handle(self):
+        """Handle the websocket connection."""
+        wsock = self.wsock = web.WebSocketResponse()
+        yield from wsock.prepare(self.request)
+
+        # Set up to cancel this connection when Home Assistant shuts down
+        self.socket_task = asyncio.Task.current_task(loop=self.hass.loop)
+        self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP,
+                                   self._cancel_connection)
+
+        self.debug('Connected')
+
+        msg = None
+        authenticated = False
+
+        try:
+            if self.request[KEY_AUTHENTICATED]:
+                authenticated = True
+
+            else:
+                self.send_message(auth_required_message())
+                msg = yield from wsock.receive_json()
+                msg = AUTH_MESSAGE_SCHEMA(msg)
+
+                if validate_password(self.request, msg['api_password']):
+                    authenticated = True
+
+                else:
+                    self.debug('Invalid password')
+                    self.send_message(auth_invalid_message('Invalid password'))
+                    return wsock
+
+            if not authenticated:
+                return wsock
+
+            self.send_message(auth_ok_message())
+
+            msg = yield from wsock.receive_json()
+
+            last_id = 0
+
+            while msg:
+                self.debug('Received', msg)
+                msg = BASE_COMMAND_MESSAGE_SCHEMA(msg)
+                cur_id = msg['id']
+
+                if cur_id <= last_id:
+                    self.send_message(error_message(
+                        cur_id, ERR_ID_REUSE,
+                        'Identifier values have to increase.'))
+
+                else:
+                    handler_name = 'handle_{}'.format(msg['type'])
+                    getattr(self, handler_name)(msg)
+
+                last_id = cur_id
+                msg = yield from wsock.receive_json()
+
+        except vol.Invalid as err:
+            error_msg = 'Message incorrectly formatted: '
+            if msg:
+                error_msg += humanize_error(msg, err)
+            else:
+                error_msg += str(err)
+
+            self.log_error(error_msg)
+
+            if not authenticated:
+                self.send_message(auth_invalid_message(error_msg))
+
+            else:
+                if isinstance(msg, dict):
+                    iden = msg.get('id')
+                else:
+                    iden = None
+
+                self.send_message(error_message(iden, ERR_INVALID_FORMAT,
+                                                error_msg))
+
+        except TypeError as err:
+            if wsock.closed:
+                self.debug('Connection closed by client')
+            else:
+                self.log_error('Unexpected TypeError', msg)
+
+        except ValueError as err:
+            msg = 'Received invalid JSON'
+            value = getattr(err, 'doc', None)  # Py3.5+ only
+            if value:
+                msg += ': {}'.format(value)
+            self.log_error(msg)
+
+        except asyncio.CancelledError:
+            self.debug('Connection cancelled by server')
+
+        except Exception:  # pylint: disable=broad-except
+            error = 'Unexpected error inside websocket API. '
+            if msg is not None:
+                error += str(msg)
+            _LOGGER.exception(error)
+
+        finally:
+            for unsub in self.event_listeners.values():
+                unsub()
+
+            yield from wsock.close()
+            self.debug('Closed connection')
+
+        return wsock
+
+    def handle_subscribe_events(self, msg):
+        """Handle subscribe events command."""
+        msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg)
+
+        self.event_listeners[msg['id']] = self.hass.bus.async_listen(
+            msg['event_type'], partial(self._forward_event, msg['id']))
+
+        self.send_message(result_message(msg['id']))
+
+    def handle_unsubscribe_events(self, msg):
+        """Handle unsubscribe events command."""
+        msg = UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg)
+
+        subscription = msg['subscription']
+
+        if subscription not in self.event_listeners:
+            self.send_message(error_message(
+                msg['id'], ERR_NOT_FOUND,
+                'Subscription not found.'))
+        else:
+            self.event_listeners.pop(subscription)()
+            self.send_message(result_message(msg['id']))
+
+    def handle_call_service(self, msg):
+        """Handle call service command."""
+        msg = CALL_SERVICE_MESSAGE_SCHEMA(msg)
+
+        self.hass.async_add_job(self._call_service_helper(msg))
+
+    def handle_get_states(self, msg):
+        """Handle get states command."""
+        msg = GET_STATES_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(msg['id'],
+                                         self.hass.states.async_all()))
+
+    def handle_get_services(self, msg):
+        """Handle get services command."""
+        msg = GET_SERVICES_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(msg['id'],
+                                         api.async_services_json(self.hass)))
+
+    def handle_get_config(self, msg):
+        """Handle get config command."""
+        msg = GET_CONFIG_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(msg['id'],
+                                         self.hass.config.as_dict()))
+
+    def handle_get_panels(self, msg):
+        """Handle get panels command."""
+        msg = GET_PANELS_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(
+            msg['id'], self.hass.data[frontend.DATA_PANELS]))
diff --git a/requirements_test.txt b/requirements_test.txt
index 838e4c96875..2dbc98326db 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -12,5 +12,6 @@ pytest-asyncio>=0.5.0
 pytest-cov>=2.3.1
 pytest-timeout>=1.2.0
 pytest-catchlog>=1.2.2
+pytest-sugar>=0.7.1
 requests_mock>=1.0
 mock-open>=1.3.1
diff --git a/tests/common.py b/tests/common.py
index fc779e120f8..25a674dd995 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -3,8 +3,7 @@ import asyncio
 import os
 import sys
 from datetime import timedelta
-from unittest import mock
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
 from io import StringIO
 import logging
 import threading
@@ -26,7 +25,7 @@ from homeassistant.const import (
 from homeassistant.components import sun, mqtt
 from homeassistant.components.http.auth import auth_middleware
 from homeassistant.components.http.const import (
-    KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED)
+    KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED, KEY_TRUSTED_NETWORKS)
 
 _TEST_INSTANCE_PORT = SERVER_PORT
 _LOGGER = logging.getLogger(__name__)
@@ -207,7 +206,7 @@ def mock_state_change_event(hass, new_state, old_state=None):
 
 def mock_http_component(hass):
     """Mock the HTTP component."""
-    hass.http = mock.MagicMock()
+    hass.http = MagicMock()
     hass.config.components.append('http')
     hass.http.views = {}
 
@@ -222,19 +221,20 @@ def mock_http_component(hass):
     hass.http.register_view = mock_register_view
 
 
-def mock_http_component_app(hass):
+def mock_http_component_app(hass, api_password=None):
     """Create an aiohttp.web.Application instance for testing."""
-    hass.http.api_password = None
+    hass.http = MagicMock(api_password=api_password)
     app = web.Application(middlewares=[auth_middleware], loop=hass.loop)
     app['hass'] = hass
     app[KEY_USE_X_FORWARDED_FOR] = False
     app[KEY_BANS_ENABLED] = False
+    app[KEY_TRUSTED_NETWORKS] = []
     return app
 
 
 def mock_mqtt_component(hass):
     """Mock the MQTT component."""
-    with mock.patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
+    with patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
         setup_component(hass, mqtt.DOMAIN, {
             mqtt.DOMAIN: {
                 mqtt.CONF_BROKER: 'mock-broker',
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
new file mode 100644
index 00000000000..5b70f0cde10
--- /dev/null
+++ b/tests/components/test_websocket_api.py
@@ -0,0 +1,285 @@
+import asyncio
+from unittest.mock import patch
+
+from aiohttp import WSMsgType
+from async_timeout import timeout
+import pytest
+
+from homeassistant.core import callback
+from homeassistant.components import websocket_api as wapi, api, frontend
+
+from tests.common import mock_http_component_app
+
+API_PASSWORD = 'test1234'
+
+
+@pytest.fixture
+def websocket_client(loop, hass, test_client):
+    """Websocket client fixture connected to websocket server."""
+    websocket_app = mock_http_component_app(hass)
+    wapi.WebsocketAPIView().register(websocket_app.router)
+
+    client = loop.run_until_complete(test_client(websocket_app))
+    ws = loop.run_until_complete(client.ws_connect(wapi.URL))
+
+    auth_ok = loop.run_until_complete(ws.receive_json())
+    assert auth_ok['type'] == wapi.TYPE_AUTH_OK
+
+    yield ws
+
+    if not ws.closed:
+        loop.run_until_complete(ws.close())
+
+
+@pytest.fixture
+def no_auth_websocket_client(hass, loop, test_client):
+    """Websocket connection that requires authentication."""
+    websocket_app = mock_http_component_app(hass, API_PASSWORD)
+    wapi.WebsocketAPIView().register(websocket_app.router)
+
+    client = loop.run_until_complete(test_client(websocket_app))
+    ws = loop.run_until_complete(client.ws_connect(wapi.URL))
+
+    auth_ok = loop.run_until_complete(ws.receive_json())
+    assert auth_ok['type'] == wapi.TYPE_AUTH_REQUIRED
+
+    yield ws
+
+    if not ws.closed:
+        loop.run_until_complete(ws.close())
+
+
+@asyncio.coroutine
+def test_auth_via_msg(no_auth_websocket_client):
+    """Test authenticating."""
+    no_auth_websocket_client.send_json({
+        'type': wapi.TYPE_AUTH,
+        'api_password': API_PASSWORD
+    })
+
+    msg = yield from no_auth_websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_AUTH_OK
+
+
+@asyncio.coroutine
+def test_auth_via_msg_incorrect_pass(no_auth_websocket_client):
+    """Test authenticating."""
+    no_auth_websocket_client.send_json({
+        'type': wapi.TYPE_AUTH,
+        'api_password': API_PASSWORD + 'wrong'
+    })
+
+    msg = yield from no_auth_websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_AUTH_INVALID
+    assert msg['message'] == 'Invalid password'
+
+
+@asyncio.coroutine
+def test_pre_auth_only_auth_allowed(no_auth_websocket_client):
+    """Verify that before authentication, only auth messages are allowed."""
+    no_auth_websocket_client.send_json({
+        'type': wapi.TYPE_CALL_SERVICE,
+        'domain': 'domain_test',
+        'service': 'test_service',
+        'service_data': {
+            'hello': 'world'
+        }
+    })
+
+    msg = yield from no_auth_websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_AUTH_INVALID
+    assert msg['message'].startswith('Message incorrectly formatted')
+
+
+@asyncio.coroutine
+def test_invalid_message_format(websocket_client):
+    """Test sending invalid JSON."""
+    websocket_client.send_json({'type': 5})
+
+    msg = yield from websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_RESULT
+    error = msg['error']
+    assert error['code'] == wapi.ERR_INVALID_FORMAT
+    assert error['message'].startswith('Message incorrectly formatted')
+
+
+@asyncio.coroutine
+def test_invalid_json(websocket_client):
+    """Test sending invalid JSON."""
+    websocket_client.send_str('this is not JSON')
+
+    msg = yield from websocket_client.receive()
+
+    assert msg.type == WSMsgType.close
+
+
+@asyncio.coroutine
+def test_quiting_hass(hass, websocket_client):
+    """Test sending invalid JSON."""
+    with patch.object(hass.loop, 'stop'):
+        yield from hass.async_stop()
+
+    msg = yield from websocket_client.receive()
+
+    assert msg.type == WSMsgType.CLOSE
+
+
+@asyncio.coroutine
+def test_call_service(hass, websocket_client):
+    """Test call service command."""
+    calls = []
+
+    @callback
+    def service_call(call):
+        calls.append(call)
+
+    hass.services.async_register('domain_test', 'test_service', service_call)
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_CALL_SERVICE,
+        'domain': 'domain_test',
+        'service': 'test_service',
+        'service_data': {
+            'hello': 'world'
+        }
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    assert len(calls) == 1
+    call = calls[0]
+
+    assert call.domain == 'domain_test'
+    assert call.service == 'test_service'
+    assert call.data == {'hello': 'world'}
+
+
+@asyncio.coroutine
+def test_subscribe_unsubscribe_events(hass, websocket_client):
+    """Test subscribe/unsubscribe events command."""
+    init_count = sum(hass.bus.async_listeners().values())
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_SUBSCRIBE_EVENTS,
+        'event_type': 'test_event'
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    # Verify we have a new listener
+    assert sum(hass.bus.async_listeners().values()) == init_count + 1
+
+    hass.bus.async_fire('ignore_event')
+    hass.bus.async_fire('test_event', {'hello': 'world'})
+    hass.bus.async_fire('ignore_event')
+
+    with timeout(3, loop=hass.loop):
+        msg = yield from websocket_client.receive_json()
+
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_EVENT
+    event = msg['event']
+
+    assert event['event_type'] == 'test_event'
+    assert event['data'] == {'hello': 'world'}
+    assert event['origin'] == 'LOCAL'
+
+    websocket_client.send_json({
+        'id': 6,
+        'type': wapi.TYPE_UNSUBSCRIBE_EVENTS,
+        'subscription': 5
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 6
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    # Check our listener got unsubscribed
+    assert sum(hass.bus.async_listeners().values()) == init_count
+
+
+@asyncio.coroutine
+def test_get_states(hass, websocket_client):
+    """ Test get_states command."""
+    hass.states.async_set('greeting.hello', 'world')
+    hass.states.async_set('greeting.bye', 'universe')
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_STATES,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    states = []
+    for state in hass.states.async_all():
+        state = state.as_dict()
+        state['last_changed'] = state['last_changed'].isoformat()
+        state['last_updated'] = state['last_updated'].isoformat()
+        states.append(state)
+
+    assert msg['result'] == states
+
+
+@asyncio.coroutine
+def test_get_services(hass, websocket_client):
+    """ Test get_services command."""
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_SERVICES,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+    assert msg['result'] == api.async_services_json(hass)
+
+
+@asyncio.coroutine
+def test_get_config(hass, websocket_client):
+    """ Test get_config command."""
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_CONFIG,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+    assert msg['result'] == hass.config.as_dict()
+
+
+@asyncio.coroutine
+def test_get_panels(hass, websocket_client):
+    """ Test get_panels command."""
+    frontend.register_built_in_panel(hass, 'map', 'Map',
+                                     'mdi:account-location')
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_PANELS,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+    assert msg['result'] == hass.data[frontend.DATA_PANELS]
diff --git a/tox.ini b/tox.ini
index 609e17087b0..1cf402468b5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@ setenv =
     LANG=en_US.UTF-8
     PYTHONPATH = {toxinidir}:{toxinidir}/homeassistant
 commands =
-     py.test -v --timeout=30 --duration=10 --cov --cov-report= {posargs}
+     py.test --timeout=30 --duration=10 --cov --cov-report= {posargs}
 deps =
      -r{toxinidir}/requirements_all.txt
      -r{toxinidir}/requirements_test.txt

From 5d2b7a6e0b5124009ba1c59aaac626cf1fb91b88 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen 
Date: Sat, 26 Nov 2016 23:22:34 -0800
Subject: [PATCH 063/137] Add ping to websockets API (#4592)

---
 homeassistant/components/websocket_api.py | 34 ++++++++++++++++++-----
 tests/components/test_websocket_api.py    | 13 +++++++++
 2 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
index 391c27e8894..357a677e5cc 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -29,18 +29,20 @@ ERR_INVALID_FORMAT = 2
 ERR_NOT_FOUND = 3
 
 TYPE_AUTH = 'auth'
+TYPE_AUTH_INVALID = 'auth_invalid'
 TYPE_AUTH_OK = 'auth_ok'
 TYPE_AUTH_REQUIRED = 'auth_required'
-TYPE_AUTH_INVALID = 'auth_invalid'
-TYPE_EVENT = 'event'
-TYPE_SUBSCRIBE_EVENTS = 'subscribe_events'
-TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events'
 TYPE_CALL_SERVICE = 'call_service'
-TYPE_GET_STATES = 'get_states'
-TYPE_GET_SERVICES = 'get_services'
+TYPE_EVENT = 'event'
 TYPE_GET_CONFIG = 'get_config'
 TYPE_GET_PANELS = 'get_panels'
+TYPE_GET_SERVICES = 'get_services'
+TYPE_GET_STATES = 'get_states'
+TYPE_PING = 'ping'
+TYPE_PONG = 'pong'
 TYPE_RESULT = 'result'
+TYPE_SUBSCRIBE_EVENTS = 'subscribe_events'
+TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events'
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -91,6 +93,11 @@ GET_PANELS_MESSAGE_SCHEMA = vol.Schema({
     vol.Required('type'): TYPE_GET_PANELS,
 })
 
+PING_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_PING,
+})
+
 BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({
     vol.Required('id'): cv.positive_int,
     vol.Required('type'): vol.Any(TYPE_CALL_SERVICE,
@@ -99,7 +106,8 @@ BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({
                                   TYPE_GET_STATES,
                                   TYPE_GET_SERVICES,
                                   TYPE_GET_CONFIG,
-                                  TYPE_GET_PANELS)
+                                  TYPE_GET_PANELS,
+                                  TYPE_PING)
 }, extra=vol.ALLOW_EXTRA)
 
 
@@ -149,6 +157,14 @@ def error_message(iden, code, message):
     }
 
 
+def pong_message(iden):
+    """Return a pong message."""
+    return {
+        'id': iden,
+        'type': TYPE_PONG,
+    }
+
+
 def result_message(iden, result=None):
     """Return a success result message."""
     return {
@@ -399,3 +415,7 @@ class ActiveConnection:
 
         self.send_message(result_message(
             msg['id'], self.hass.data[frontend.DATA_PANELS]))
+
+    def handle_ping(self, msg):
+        """Handle ping command."""
+        self.send_message(pong_message(msg['id']))
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
index 5b70f0cde10..75c33110580 100644
--- a/tests/components/test_websocket_api.py
+++ b/tests/components/test_websocket_api.py
@@ -283,3 +283,16 @@ def test_get_panels(hass, websocket_client):
     assert msg['type'] == wapi.TYPE_RESULT
     assert msg['success']
     assert msg['result'] == hass.data[frontend.DATA_PANELS]
+
+
+@asyncio.coroutine
+def test_ping(websocket_client):
+    """ Test get_panels command."""
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_PING,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_PONG

From 464e8431864b38da61f49cc92455907f3800daaf Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen 
Date: Sat, 26 Nov 2016 23:44:20 -0800
Subject: [PATCH 064/137] Update frontend

---
 homeassistant/components/frontend/version.py  |  11 ++++++-----
 .../components/frontend/www_static/core.js    |   8 ++++----
 .../components/frontend/www_static/core.js.gz | Bin 32810 -> 33404 bytes
 .../frontend/www_static/frontend.html         |   4 ++--
 .../frontend/www_static/frontend.html.gz      | Bin 130460 -> 130153 bytes
 .../www_static/home-assistant-polymer         |   2 +-
 .../www_static/panels/ha-panel-dev-event.html |   2 +-
 .../panels/ha-panel-dev-event.html.gz         | Bin 2656 -> 2656 bytes
 .../www_static/panels/ha-panel-logbook.html   |   2 +-
 .../panels/ha-panel-logbook.html.gz           | Bin 7344 -> 7344 bytes
 .../www_static/panels/ha-panel-map.html       |   4 ++--
 .../www_static/panels/ha-panel-map.html.gz    | Bin 43913 -> 42075 bytes
 .../frontend/www_static/service_worker.js     |   2 +-
 .../frontend/www_static/service_worker.js.gz  | Bin 2325 -> 2325 bytes
 .../www_static/websocket_test.html.gz         | Bin 0 -> 1117 bytes
 15 files changed, 18 insertions(+), 17 deletions(-)
 create mode 100644 homeassistant/components/frontend/www_static/websocket_test.html.gz

diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py
index 1a0dac2f3bc..92cf35a7803 100644
--- a/homeassistant/components/frontend/version.py
+++ b/homeassistant/components/frontend/version.py
@@ -1,17 +1,18 @@
 """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
 
 FINGERPRINTS = {
-    "core.js": "5ed5e063d66eb252b5b288738c9c2d16",
-    "frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d",
+    "core.js": "525498104891894d97cbf0caf7291edc",
+    "frontend.html": "18667e347b85a368724308bb1b9485b4",
     "mdi.html": "46a76f877ac9848899b8ed382427c16f",
     "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
-    "panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
+    "panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
     "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
     "panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769",
     "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
     "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
     "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
     "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
-    "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
-    "panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e"
+    "panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
+    "panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
+    "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
 }
diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js
index a07e5819489..6380a6fcaf1 100644
--- a/homeassistant/components/frontend/www_static/core.js
+++ b/homeassistant/components/frontend/www_static/core.js
@@ -1,4 +1,4 @@
-!(function(){"use strict";function t(t){return t&&t.__esModule?t.default:t}function e(t,e){return e={exports:{}},t(e,e.exports),e.exports}function n(t,e){var n=e.authToken,r=e.host;return Ne({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function r(){return ke.getInitialState()}function i(t,e){var n=e.errorMessage;return t.withMutations((function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)}))}function o(t,e){var n=e.authToken,r=e.host;return Pe({authToken:n,host:r})}function u(){return He.getInitialState()}function a(t,e){var n=e.rememberAuth;return n}function s(t){return t.withMutations((function(t){t.set("isStreaming",!0).set("useStreaming",!0).set("hasError",!1)}))}function c(t){return t.withMutations((function(t){t.set("isStreaming",!1).set("useStreaming",!1).set("hasError",!1)}))}function f(t){return t.withMutations((function(t){t.set("isStreaming",!1).set("hasError",!0)}))}function h(){return Be.getInitialState()}function l(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?tn({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||tn;return t.set(o,u.withMutations((function(t){for(var e=0;e199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function D(t,e){var n=e.message;return t.set(t.size,n)}function z(){return zn.getInitialState()}function R(t,e){t.dispatch(An.NOTIFICATION_CREATED,{message:e})}function L(t){t.registerStores({notifications:zn})}function M(t,e){if("lock"===t)return!0;if("garage_door"===t)return!0;var n=e.get(t);return!!n&&n.services.has("turn_on")}function j(t,e){return!!t&&("group"===t.domain?"on"===t.state||"off"===t.state:M(t.domain,e))}function N(t,e){return[rr(t),function(t){return!!t&&t.services.has(e)}]}function k(t){return[wn.byId(t),nr,j]}function U(t,e,n){function r(){var c=(new Date).getTime()-a;c0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function P(t,e){var n=e.component;return t.push(n)}function H(t,e){var n=e.components;return dr(n)}function x(){return vr.getInitialState()}function V(t,e){var n=e.latitude,r=e.longitude,i=e.location_name,o=e.unit_system,u=e.time_zone,a=e.config_dir,s=e.version;return Sr({latitude:n,longitude:r,location_name:i,unit_system:o,time_zone:u,config_dir:a,serverVersion:s})}function F(){return gr.getInitialState()}function q(t,e){t.dispatch(pr.SERVER_CONFIG_LOADED,e)}function G(t){ln(t,"GET","config").then((function(e){return q(t,e)}))}function K(t,e){t.dispatch(pr.COMPONENT_LOADED,{component:e})}function B(t){return[["serverComponent"],function(e){return e.contains(t)}]}function Y(t){t.registerStores({serverComponent:vr,serverConfig:gr})}function J(t,e){var n=e.pane;return n}function W(){return Rr.getInitialState()}function X(t,e){var n=e.panels;return Mr(n)}function Q(){return jr.getInitialState()}function Z(t,e){var n=e.show;return!!n}function $(){return kr.getInitialState()}function tt(t,e){t.dispatch(Dr.SHOW_SIDEBAR,{show:e})}function et(t,e){t.dispatch(Dr.NAVIGATE,{pane:e})}function nt(t,e){t.dispatch(Dr.PANELS_LOADED,{panels:e})}function rt(t,e){var n=e.entityId;return n}function it(){return Kr.getInitialState()}function ot(t,e){t.dispatch(qr.SELECT_ENTITY,{entityId:e})}function ut(t){t.dispatch(qr.SELECT_ENTITY,{entityId:null})}function at(t){return!t||(new Date).getTime()-t>6e4}function st(t,e){var n=e.date;return n.toISOString()}function ct(){return Wr.getInitialState()}function ft(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Qr({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],Qr(e.map(yn.fromJSON)))}))}))}function ht(){return Zr.getInitialState()}function lt(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,ni(e.map(yn.fromJSON)))}))}))}function pt(){return ri.getInitialState()}function _t(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(ui,r)}))}function dt(){return ai.getInitialState()}function vt(t,e){t.dispatch(Yr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function yt(t,e){void 0===e&&(e=null),t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),ln(t,"GET",n).then((function(e){return t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function St(t,e){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_START,{date:e}),ln(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function gt(t){var e=t.evaluate(fi);return St(t,e)}function mt(t){t.registerStores({currentEntityHistoryDate:Wr,entityHistory:Zr,isLoadingEntityHistory:ti,recentEntityHistory:ri,recentEntityHistoryUpdated:ai})}function Et(t){t.registerStores({moreInfoEntityId:Kr})}function It(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;oau}function se(t){t.registerStores({currentLogbookDate:Yo,isLoadingLogbookEntries:Wo,logbookEntries:eu,logbookEntriesUpdated:iu})}function ce(t){return t.set("active",!0)}function fe(t){return t.set("active",!1)}function he(){return gu.getInitialState()}function le(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",ln(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(vu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),Nn.createNotification(t,n),!1}))}function pe(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return ln(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(vu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Nn.createNotification(t,n),!1}))}function _e(t){t.registerStores({pushNotifications:gu})}function de(t,e){return ln(t,"POST","template",{template:e})}function ve(t){return t.set("isListening",!0)}function ye(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function Se(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function ge(){return Nu.getInitialState()}function me(){return Nu.getInitialState()}function Ee(){return Nu.getInitialState()}function Ie(t){return ku[t.hassId]}function be(t){var e=Ie(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(Lu.VOICE_TRANSMITTING,{finalTranscript:n}),ur.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(Lu.VOICE_DONE)}),(function(){t.dispatch(Lu.VOICE_ERROR)}))}}function Oe(t){var e=Ie(t);e&&(e.recognition.stop(),ku[t.hassId]=!1)}function we(t){be(t),Oe(t)}function Te(t){var e=we.bind(null,t);e();var n=new webkitSpeechRecognition;ku[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(Lu.VOICE_START)},n.onerror=function(){return t.dispatch(Lu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ie(t);if(n){for(var r="",i="",o=e.resultIndex;o>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?_(t)+e:e}function v(){return!0}function y(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function S(t,e){return m(t,e,0)}function g(t,e){return m(t,e,e)}function m(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function I(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function b(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(bn&&t[bn]||t[On]);if("function"==typeof e)return e}function C(t){return t&&"number"==typeof t.length}function D(t){return null===t||void 0===t?P():o(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?P().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?P():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():x(t)}function L(t){return(null===t||void 0===t?P():o(t)?u(t)?t.entrySeq():t:x(t)).toSetSeq()}function M(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function N(t){this._iterable=t,this.size=t.length||t.size}function k(t){this._iterator=t,this._iteratorCache=[]}function U(t){return!(!t||!t[Tn])}function P(){return An||(An=new M([]))}function H(t){var e=Array.isArray(t)?new M(t).fromEntrySeq():w(t)?new k(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=F(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=F(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function F(t){return C(t)?new M(t):w(t)?new k(t):O(t)?new N(t):void 0}function q(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?b():I(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?B(e,t,"",{"":t}):Y(t)}function B(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map((function(n,r){return B(t,n,r,e)}))):J(e)?t.call(r,n,z(e).map((function(n,r){return B(t,n,r,e)}))):e}function Y(t){return Array.isArray(t)?R(t).map(Y).toList():J(t)?z(t).map(Y).toMap():t}function J(t){return t&&(t.constructor===Object||void 0===t.constructor)}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!s(t);if(c(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var h=!0,l=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,yn)):!W(t.get(r,yn),e))return h=!1,!1}));return h&&t.size===l}function Q(t,e){if(!(this instanceof Q))return new Q(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Cn)return Cn;Cn=this}}function Z(t,e){if(!t)throw new Error(e)}function $(t,e,n){if(!(this instanceof $))return new $(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>Un?ut(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return st(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=xn[t];return void 0===e&&(e=at(t),Hn===Pn&&(Hn=0,xn={}),Hn++,xn[t]=e),e}function at(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ht(t){return null===t||void 0===t?It():lt(t)&&!c(t)?t:It().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function lt(t){return!(!t||!t[Vn])}function pt(t,e){this.ownerID=t,this.entries=e}function _t(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function dt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function St(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&mt(t._root)}function gt(t,e){return I(t,e[0],e[1])}function mt(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(Fn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function It(){return qn||(qn=Et(0))}function bt(t,e,n){var r,i;if(t._root){var o=f(Sn),u=f(gn);if(r=Ot(t._root,t.__ownerID,0,void 0,e,n,o,u),!u.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new pt(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):It()}function Ot(t,e,n,r,i,o,u,a){return t?t.update(e,n,r,i,o,u,a):o===yn?t:(h(a),h(u),new yt(e,r,[i,o]))}function wt(t){return t.constructor===yt||t.constructor===vt}function Tt(t,e,n,r,i){if(t.keyHash===r)return new vt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&vn,a=(0===n?r:r>>>n)&vn,s=u===a?[Tt(t,e,n+_n,r,i)]:(o=new yt(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new dt(t,o+1,u)}function zt(t,e,r){for(var i=[],u=0;u>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function kt(t,e,n,r){var i=r?t:p(t);return i[e]=n,i}function Ut(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&io?0:o-n,c=u-n;return c>dn&&(c=dn),function(){if(i===c)return Xn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>dn&&(f=dn),function(){for(;;){if(a){var t=a();if(t!==Xn)return t;a=null}if(c===f)return Xn;var o=e?--f:c++;a=n(s&&s[o],r-_n,i+(o<=t.size||e<0)return t.withMutations((function(t){e<0?Wt(t,e).set(0,n):Wt(t,0,e+1).set(e,n)}));e+=t._origin;var r=t._tail,i=t._root,o=f(gn);return e>=Qt(t._capacity)?r=Bt(r,t.__ownerID,0,e,n,o):i=Bt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):qt(t._origin,t._capacity,t._level,i,r):t}function Bt(t,e,n,r,i,o){var u=r>>>n&vn,a=t&&u0){var c=t&&t.array[u],f=Bt(c,e,n-_n,r,i,o);return f===c?t:(s=Yt(t,e),s.array[u]=f,s)}return a&&t.array[u]===i?t:(h(o),s=Yt(t,e),void 0===i&&u===s.array.length-1?s.array.pop():s.array[u]=i,s)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Vt(t?t.array.slice():[],e)}function Jt(t,e){if(e>=Qt(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&vn],r-=_n;return n}}function Wt(t,e,n){void 0!==e&&(e=0|e),void 0!==n&&(n=0|n);var r=t.__ownerID||new l,i=t._origin,o=t._capacity,u=i+e,a=void 0===n?o:n<0?o+n:i+n;if(u===i&&a===o)return t;if(u>=a)return t.clear();for(var s=t._level,c=t._root,f=0;u+f<0;)c=new Vt(c&&c.array.length?[void 0,c]:[],r),s+=_n,f+=1<=1<h?new Vt([],r):_;if(_&&p>h&&u_n;y-=_n){var S=h>>>y&vn;v=v.array[S]=Yt(v.array[S],r)}v.array[h>>>_n&vn]=_}if(a=p)u-=p,a-=p,s=_n,c=null,d=d&&d.removeBefore(r,0,u);else if(u>i||p>>s&vn;if(g!==p>>>s&vn)break;g&&(f+=(1<i&&(c=c.removeBefore(r,s,u-f)),c&&pu&&(u=c.size),o(s)||(c=c.map((function(t){return K(t)}))),i.push(c)}return u>t.size&&(t=t.setSize(u)),Mt(t,e,i)}function Qt(t){return t>>_n<<_n}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function $t(t){return lt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Qn||(Qn=te(It(),Gt()))}function ne(t,e,n){var r,i,o=t._map,u=t._list,a=o.get(e),s=void 0!==a;if(n===yn){if(!s)return t;u.size>=dn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function ae(t){var e=Ce(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=De,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===In){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?mn:En,n)},e}function se(t,e,n){var r=Ce(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(In,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return I(r,a,e.call(n,u[1],a,t),i)})},r}function ce(t,e){var n=Ce(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=De,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function fe(t,e,n,r){var i=Ce(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(In,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return I(i,r?c:a++,f,o)}})},i}function he(t,e,n){var r=ht().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function le(t,e,n){var r=u(t),i=(c(t)?Zt():ht()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Ae(t);return i.map((function(e){return Oe(t,o(e))}))}function pe(t,e,n,r){var i=t.size;if(void 0!==e&&(e=0|e),void 0!==n&&(n=n===1/0?i:0|n),y(e,n,i))return t;var o=S(e,i),u=g(n,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,n,r);var a,s=u-o;s===s&&(a=s<0?0:s);var c=Ce(t);return c.size=0===a?a:t.size&&a||void 0,!r&&U(t)&&a>=0&&(c.get=function(e,n){return e=d(this,e),e>=0&&ea)return b();var t=i.next();return r||e===En?t:e===mn?I(e,s-1,void 0,t):I(e,s-1,t.value[1],t)})},c}function _e(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(In,i),a=!0;return new E(function(){if(!a)return b();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===In?t:I(r,s,c,t):(a=!1,b())})},r}function de(t,e,n,r){var i=Ce(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(In,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===mn?I(i,c++,void 0,t):I(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===In?t:I(i,o,f,t)})},i}function ve(t,e){var r=u(t),i=[t].concat(e).map((function(t){return o(t)?r&&(t=n(t)):t=r?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===i.length)return t;if(1===i.length){var s=i[0];if(s===t||r&&u(s)||a(t)&&a(s))return s}var c=new M(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),c}function ye(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){function u(t,c){var f=this;t.__iterate((function(t,i){return(!e||c0}function be(t,n,r){var i=Ce(t);return i.size=new M(r).map((function(t){return t.size})).min(),i.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(En,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},i.__iteratorUncached=function(t,i){var o=r.map((function(t){return t=e(t),T(i?t.reverse():t)})),u=0,a=!1;return new E(function(){var e;return a||(e=o.map((function(t){return t.next()})),a=e.some((function(t){return t.done}))),a?b():I(t,u++,n.apply(null,e.map((function(t){return t.value}))))})},i}function Oe(t,e){return U(t)?e:t.constructor(e)}function we(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Te(t){return ft(t.size),_(t)}function Ae(t){return u(t)?n:a(t)?r:i}function Ce(t){return Object.create((u(t)?z:a(t)?R:L).prototype)}function De(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):D.prototype.cacheResult.call(this)}function ze(t,e){return t>e?1:te?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=u(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0});return un(i,r)}function un(t,e){return e=Rn(e,3432918353),e=Rn(e<<15|e>>>-15,461845907),e=Rn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Rn(e^e>>>16,2246822507),e=Rn(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=a,e.isAssociative=s,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",fn="@@__IMMUTABLE_KEYED__@@",hn="@@__IMMUTABLE_INDEXED__@@",ln="@@__IMMUTABLE_ORDERED__@@",pn="delete",_n=5,dn=1<<_n,vn=dn-1,yn={},Sn={value:!1},gn={value:!1},mn=0,En=1,In=2,bn="function"==typeof Symbol&&Symbol.iterator,On="@@iterator",wn=bn||On;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=mn,E.VALUES=En,E.ENTRIES=In,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(D,e),D.of=function(){return D(arguments)},D.prototype.toSeq=function(){return this},D.prototype.toString=function(){return this.__toString("Seq {","}")},D.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},D.prototype.__iterate=function(t,e){return q(this,t,e,!0)},D.prototype.__iterator=function(t,e){return G(this,t,e,!0)},t(z,D),z.prototype.toKeyedSeq=function(){return this},t(R,D),R.of=function(){return R(arguments)},R.prototype.toIndexedSeq=function(){return this},R.prototype.toString=function(){return this.__toString("Seq [","]")},R.prototype.__iterate=function(t,e){return q(this,t,e,!1)},R.prototype.__iterator=function(t,e){return G(this,t,e,!1)},t(L,D),L.of=function(){return L(arguments)},L.prototype.toSetSeq=function(){return this},D.isSeq=U,D.Keyed=z,D.Set=L,D.Indexed=R;var Tn="@@__IMMUTABLE_SEQ__@@";D.prototype[Tn]=!0,t(M,R),M.prototype.get=function(t,e){return this.has(t)?this._array[d(this,t)]:e},M.prototype.__iterate=function(t,e){for(var n=this,r=this._array,i=r.length-1,o=0;o<=i;o++)if(t(r[e?i-o:o],o,n)===!1)return o+1;return o},M.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?b():I(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?b():I(t,u,n[u])})},j.prototype[ln]=!0,t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},N.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(b);var i=0;return new E(function(){var e=r.next();return e.done?e:I(t,i++,e.value)})},t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return I(t,i,r[i++])})};var An;t(Q,R),Q.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Q.prototype.get=function(t,e){return this.has(t)?this._value:e},Q.prototype.includes=function(t){return W(this._value,t)},Q.prototype.slice=function(t,e){var n=this.size;return y(t,e,n)?this:new Q(this._value,g(e,n)-S(t,n))},Q.prototype.reverse=function(){return this},Q.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Q.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Q.prototype.__iterate=function(t,e){for(var n=this,r=0;r=0&&e=0&&nn?b():I(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:X(this,t)};var Dn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var zn,Rn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t=0|t,e=0|e;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Ln=Object.isExtensible,Mn=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),jn="function"==typeof WeakMap;jn&&(zn=new WeakMap);var Nn=0,kn="__immutablehash__";"function"==typeof Symbol&&(kn=Symbol(kn));var Un=16,Pn=255,Hn=0,xn={};t(ht,et),ht.of=function(){var t=sn.call(arguments,0);return It().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},ht.prototype.toString=function(){return this.__toString("Map {","}")},ht.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ht.prototype.set=function(t,e){return bt(this,t,e)},ht.prototype.setIn=function(t,e){return this.updateIn(t,yn,(function(){return e}))},ht.prototype.remove=function(t){return bt(this,t,yn)},ht.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return yn}))},ht.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},ht.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=jt(this,Re(t),e,n);return r===yn?void 0:r},ht.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):It()},ht.prototype.merge=function(){return zt(this,void 0,arguments)},ht.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return zt(this,t,e)},ht.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,It(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},ht.prototype.mergeDeep=function(){return zt(this,Rt,arguments)},ht.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return zt(this,Lt(t),e)},ht.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,It(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},ht.prototype.sort=function(t){return Zt(me(this,t))},ht.prototype.sortBy=function(t,e){return Zt(me(this,e,t))},ht.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ht.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new l)},ht.prototype.asImmutable=function(){return this.__ensureOwner()},ht.prototype.wasAltered=function(){return this.__altered},ht.prototype.__iterator=function(t,e){return new St(this,t,e)},ht.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},ht.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ht.isMap=lt;var Vn="@@__IMMUTABLE_MAP__@@",Fn=ht.prototype;Fn[Vn]=!0,Fn[pn]=Fn.remove,Fn.removeIn=Fn.deleteIn,pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Gn)return At(t,s,r,i);var _=t&&t===this.ownerID,d=_?s:p(s);return l?a?c===f-1?d.pop():d[c]=d.pop():d[c]=[r,i]:d.push([r,i]),_?(this.entries=d,this):new pt(t,d)}},_t.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&vn),o=this.bitmap;return 0===(o&i)?r:this.nodes[Nt(o&i-1)].get(t+_n,e,n,r)},_t.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=1<=Kn)return Dt(t,l,c,a,_);if(f&&!_&&2===l.length&&wt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&wt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?kt(l,h,_,d):Pt(l,h,d):Ut(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new _t(t,v,y)},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&vn,o=this.nodes[i];return o?o.get(t+_n,e,n,r):r},dt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=i===yn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Ot(f,t,e+_n,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&vn;if(r>=this.array.length)return new Vt([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-_n,n),i===u&&o)return this}if(o&&!i)return this;var a=Yt(this,t);if(!o)for(var s=0;s>>e&vn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-_n,n),i===o&&r===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Wn,Xn={};t(Zt,ht),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=$t,Zt.prototype[ln]=!0,Zt.prototype[pn]=Zt.prototype.remove;var Qn;t(re,z),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=se(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Te(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Te(this):0;return new E(function(){var i=n.next();return i.done?i:I(t,e?--r:r++,i.value,i)})},re.prototype[ln]=!0,t(ie,R),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:I(t,r++,e.value,e)})},t(oe,L),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:I(t,e.value,e.value,e)})},t(ue,z),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){we(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ue.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){we(r);var i=o(r);return I(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=De,t(Le,et),Le.prototype.toString=function(){return this.__toString(je(this)+" {","}")},Le.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Le.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Le.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Me(this,It()))},Le.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+je(this));if(this._map&&!this._map.has(t)){var n=this._defaultValues[t];if(e===n)return this}var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:Me(this,r)},Le.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Me(this,e)},Le.prototype.wasAltered=function(){return this._map.wasAltered()},Le.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Le.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Le.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Me(this,e,t):(this.__ownerID=t,this._map=e,this)};var Zn=Le.prototype;Zn[pn]=Zn.remove,Zn.deleteIn=Zn.removeIn=Fn.removeIn,Zn.merge=Fn.merge,Zn.mergeWith=Fn.mergeWith,Zn.mergeIn=Fn.mergeIn,Zn.mergeDeep=Fn.mergeDeep,Zn.mergeDeepWith=Fn.mergeDeepWith,Zn.mergeDeepIn=Fn.mergeDeepIn,Zn.setIn=Fn.setIn,Zn.update=Fn.update,Zn.updateIn=Fn.updateIn,Zn.withMutations=Fn.withMutations,Zn.asMutable=Fn.asMutable,Zn.asImmutable=Fn.asImmutable,t(Ue,rt),Ue.of=function(){return this(arguments)},Ue.fromKeys=function(t){return this(n(t).keySeq())},Ue.prototype.toString=function(){return this.__toString("Set {","}")},Ue.prototype.has=function(t){return this._map.has(t)},Ue.prototype.add=function(t){
-return He(this,this._map.set(t,!0))},Ue.prototype.remove=function(t){return He(this,this._map.remove(t))},Ue.prototype.clear=function(){return He(this,this._map.clear())},Ue.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ft(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pop=function(){return this.slice(1)},Be.prototype.unshift=function(){return this.push.apply(this,arguments)},Be.prototype.unshiftAll=function(t){return this.pushAll(t)},Be.prototype.shift=function(){return this.pop.apply(this,arguments)},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):We()},Be.prototype.slice=function(t,e){if(y(t,e,this.size))return this;var n=S(t,this.size),r=g(e,this.size);if(r!==this.size)return nt.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Je(i,o)},Be.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Je(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Be.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},Be.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,I(t,n++,e)}return b()})},Be.isStack=Ye;var ir="@@__IMMUTABLE_STACK__@@",or=Be.prototype;or[ir]=!0,or.withMutations=Fn.withMutations,or.asMutable=Fn.asMutable,or.asImmutable=Fn.asImmutable,or.wasAltered=Fn.wasAltered;var ur;e.Iterator=E,Xe(e,{toArray:function(){ft(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,n){t[n]=e})),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return ht(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate((function(e,n){t[n]=e})),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return Fe(u(this)?this.valueSeq():this)},toSet:function(){return Ue(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Be(u(this)?this.valueSeq():this)},toList:function(){return Ht(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sn.call(arguments,0);return Oe(this,ve(this,t))},includes:function(t){return this.some((function(e){return W(e,t)}))},entries:function(){return this.__iterator(In)},every:function(t,e){ft(this.size);var n=!0;return this.__iterate((function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1})),n},filter:function(t,e){return Oe(this,fe(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate((function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""})),e},keys:function(){return this.__iterator(mn)},map:function(t,e){return Oe(this,se(this,t,e))},reduce:function(t,e,n){ft(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate((function(e,o,u){i?(i=!1,r=e):r=t.call(n,r,e,o,u)})),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Oe(this,ce(this,!0))},slice:function(t,e){return Oe(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return Oe(this,me(this,t))},values:function(){return this.__iterator(En)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return _(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return he(this,t,e)},equals:function(t){return X(this,t)},entrySeq:function(){var t=this;if(t._cache)return new M(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter($e(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate((function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1})),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(v)},flatMap:function(t,e){return Oe(this,Se(this,t,e))},flatten:function(t){return Oe(this,ye(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find((function(e,n){return W(n,t)}),void 0,e)},getIn:function(t,e){for(var n,r=this,i=Re(t);!(n=i.next()).done;){var o=n.value;if(r=r&&r.get?r.get(o,yn):yn,r===yn)return e}return r},groupBy:function(t,e){return le(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey((function(e){return W(e,t)}))},keySeq:function(){return this.toSeq().map(Qe).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return Oe(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return Oe(this,de(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return Oe(this,me(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return Oe(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return Oe(this,_e(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var ar=e.prototype;ar[cn]=!0,ar[wn]=ar.values,ar.__toJS=ar.toArray,ar.__toStringMapper=en,ar.inspect=ar.toSource=function(){return this.toString()},ar.chain=ar.flatMap,ar.contains=ar.includes,Xe(n,{flip:function(){return Oe(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return Oe(this,this.toSeq().map((function(i,o){return t.call(e,[o,i],r++,n)})).fromEntrySeq())},mapKeys:function(t,e){var n=this;return Oe(this,this.toSeq().flip().map((function(r,i){return t.call(e,r,i,n)})).flip())}});var sr=n.prototype;sr[fn]=!0,sr[wn]=ar.entries,sr.__toJS=ar.toObject,sr.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Xe(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return Oe(this,fe(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return Oe(this,ce(this,!1))},slice:function(t,e){return Oe(this,pe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=S(t,t<0?this.count():this.size);var r=this.slice(0,t);return Oe(this,1===n?r:r.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return Oe(this,ye(this,t,!1))},get:function(t,e){return t=d(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=d(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,g.toFactory)(E),t.exports=e.default},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new C({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return m(t,[n])}))})),g(t)}))}function u(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){var r=t.get("logger");if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var i=t.get("state"),o=t.get("dirtyStores"),u=i.withMutations((function(u){r.dispatchStart(t,e,n),t.get("stores").forEach((function(i,a){var s=u.get(a),c=void 0;try{c=i.handle(s,e,n)}catch(e){throw r.dispatchError(t,e.message),e}if(void 0===c&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw r.dispatchError(t,h),new Error(h)}u.set(a,c),s!==c&&(o=o.add(a))})),r.dispatchEnd(t,u,o,i)})),a=t.set("state",u).set("dirtyStores",o).update("storeStates",(function(t){return m(t,o)}));return g(a)}function s(t,e){var n=[],r=(0,O.toImmutable)({}).withMutations((function(r){(0,A.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=I.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return m(t,n)}))}function c(t,e,n){var r=e;(0,T.isKeyPath)(e)&&(e=(0,w.fromKeyPath)(e));var i=t.get("nextId"),o=(0,w.getStoreDeps)(e),u=I.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,I.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,T.isKeyPath)(e)&&(0,T.isKeyPath)(r)?(0,T.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return m(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,T.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,w.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");var r=t.get("cache"),o=r.lookup(e),u=!o||y(t,o);return u&&(o=S(t,e)),i(o.get("value"),t.update("cache",(function(t){return u?t.miss(e,o):t.hit(e)})))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",I.default.Set())}function y(t,e){var n=e.get("storeStates");return!n.size||n.some((function(e,n){return t.getIn(["storeStates",n])!==e}))}function S(t,e){var n=(0,w.getDeps)(e).map((function(e){return _(t,e).result})),r=(0,w.getComputeFn)(e).apply(null,n),i=(0,w.getStoreDeps)(e),o=(0,O.toImmutable)({}).withMutations((function(e){i.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return(0,b.CacheEntry)({value:r,storeStates:o,dispatchId:t.get("dispatchId")})}function g(t){return t.update("dispatchId",(function(t){return t+1}))}function m(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var E=n(3),I=r(E),b=n(9),O=n(5),w=n(10),T=n(11),A=n(4),C=I.default.Record({result:null,reactorState:null})},function(t,e,n){function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){return new s}Object.defineProperty(e,"__esModule",{value:!0});var o=(function(){function t(t,e){for(var n=0;nn.dispatchId)throw new Error("Refusing to cache older value");return n})))}},{key:"evict",value:function(e){return new t(this.cache.remove(e))}}]),t})();e.BasicCache=s;var c=1e3,f=1,h=(function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1],i=arguments.length<=2||void 0===arguments[2]?new s:arguments[2],o=arguments.length<=3||void 0===arguments[3]?(0,u.OrderedSet)():arguments[3];r(this,t),console.log("using LRU"),this.limit=e,this.evictCount=n,this.cache=i,this.lru=o}return o(t,[{key:"lookup",value:function(t,e){return this.cache.lookup(t,e)}},{key:"has",value:function(t){return this.cache.has(t)}},{key:"asMap",value:function(){return this.cache.asMap()}},{key:"hit",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache,this.lru.remove(e).add(e)):this}},{key:"miss",value:function(e,n){var r;if(this.lru.size>=this.limit){if(this.has(e))return new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.remove(e).add(e));var i=this.lru.take(this.evictCount).reduce((function(t,e){return t.evict(e)}),this.cache).miss(e,n);r=new t(this.limit,this.evictCount,i,this.lru.skip(this.evictCount).add(e))}else r=new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.add(e));return r}},{key:"evict",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache.evict(e),this.lru.remove(e)):this}}]),t})();e.LRUCache=h},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(8),i={dispatchStart:function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},dispatchError:function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},dispatchEnd:function(t,e,n,i){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}};e.ConsoleGroupLogger=i;var o={dispatchStart:function(t,e,n){},dispatchError:function(t,e){},dispatchEnd:function(t,e,n){}};e.NoopLogger=o},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n(9),o=n(12),u=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=u;var a=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=a;var s=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,i.DefaultCache)(),logger:o.NoopLogger,storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:u});e.ReactorState=s;var c=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=c}])}))})),ze=t(De),Re=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},Le=Re,Me=Le({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),je=ze.Store,Ne=ze.toImmutable,ke=new je({getInitialState:function(){return Ne({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Me.VALIDATING_AUTH_TOKEN,n),this.on(Me.VALID_AUTH_TOKEN,r),this.on(Me.INVALID_AUTH_TOKEN,i)}}),Ue=ze.Store,Pe=ze.toImmutable,He=new Ue({getInitialState:function(){return Pe({authToken:null,host:""})},initialize:function(){this.on(Me.VALID_AUTH_TOKEN,o),this.on(Me.LOG_OUT,u)}}),xe=ze.Store,Ve=new xe({getInitialState:function(){return!0},initialize:function(){this.on(Me.VALID_AUTH_TOKEN,a)}}),Fe=Le({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),qe="object"==typeof window&&"EventSource"in window,Ge=ze.Store,Ke=ze.toImmutable,Be=new Ge({getInitialState:function(){return Ke({isSupported:qe,isStreaming:!1,useStreaming:!0,hasError:!1})},initialize:function(){this.on(Fe.STREAM_START,s),this.on(Fe.STREAM_STOP,c),this.on(Fe.STREAM_ERROR,f),this.on(Fe.LOG_OUT,h)}}),Ye=Le({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),Je=ze.Store,We=new Je({getInitialState:function(){return!0},initialize:function(){this.on(Ye.API_FETCH_ALL_START,(function(){return!0})),this.on(Ye.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(Ye.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(Ye.LOG_OUT,(function(){return!1}))}}),Xe=ze.Store,Qe=new Xe({getInitialState:function(){return!1},initialize:function(){this.on(Ye.SYNC_SCHEDULED,(function(){return!0})),this.on(Ye.SYNC_SCHEDULE_CANCELLED,(function(){return!1})),this.on(Ye.LOG_OUT,(function(){return!1}))}}),Ze=Le({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,
-API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),$e=ze.Store,tn=ze.toImmutable,en=new $e({getInitialState:function(){return tn({})},initialize:function(){var t=this;this.on(Ze.API_FETCH_SUCCESS,l),this.on(Ze.API_SAVE_SUCCESS,l),this.on(Ze.API_DELETE_SUCCESS,p),this.on(Ze.LOG_OUT,(function(){return t.getInitialState()}))}}),nn=Object.prototype.hasOwnProperty,rn=Object.prototype.propertyIsEnumerable,on=d()?Object.assign:function(t,e){for(var n,r,i=arguments,o=_(t),u=1;u199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function l(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?Ye({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||Ye;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function ut(t,e){var n=e.date;return n.toISOString()}function at(){return Pr.getInitialState()}function st(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Hr({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],Hr(e.map(cn.fromJSON)))}))}))}function ct(){return xr.getInitialState()}function ft(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,Gr(e.map(cn.fromJSON)))}))}))}function ht(){return Kr.getInitialState()}function lt(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(Jr,r)}))}function pt(){return Wr.getInitialState()}function _t(t,e){t.dispatch(kr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function dt(t,e){void 0===e&&(e=null),t.dispatch(kr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),Ge(t,"GET",n).then((function(e){return t.dispatch(kr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(kr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function vt(t,e){return t.dispatch(kr.ENTITY_HISTORY_FETCH_START,{date:e}),Ge(t,"GET","history/period/"+e).then((function(n){return t.dispatch(kr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(kr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function yt(t){var e=t.evaluate(Zr);return vt(t,e)}function gt(t){t.registerStores({currentEntityHistoryDate:Pr,entityHistory:xr,isLoadingEntityHistory:qr,recentEntityHistory:Kr,recentEntityHistoryUpdated:Wr})}function mt(t){t.registerStores({moreInfoEntityId:Lr})}function St(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;o0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function kt(t){var e=Wi[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),Wi[t.hassId]=!1)}function Nt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),kt(t);var r=t.evaluate(yo.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",xe(i,{authToken:r}).then((function(e){var r=jt((function(){return e.ping()}),Yi);r(),e.socket.addEventListener("message",r),Wi[t.hassId]={conn:e,scheduleHealthCheck:r},Ji.forEach((function(n){return e.subscribeEvents(Bi.bind(null,t),n)})),t.batch((function(){t.dispatch(ke.STREAM_START),n&&Fi.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(ke.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(ke.STREAM_START),Fi.fetchAll(t)}))}))}))}function Pt(t){t.registerStores({streamStatus:Ue})}function Ut(t,e,n){void 0===n&&(n={});var r=n.rememberAuth;void 0===r&&(r=!1);var i=n.host;void 0===i&&(i=""),t.dispatch(Te.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),Fi.fetchAll(t).then((function(){t.dispatch(Te.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),to.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=ro),t.dispatch(Te.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Ht(t){t.dispatch(Te.LOG_OUT,{})}function xt(t){t.registerStores({authAttempt:De,authCurrent:Me,rememberAuth:je})}function Vt(){if(!("localStorage"in window))return{};var t=window.localStorage,e="___test";try{return t.setItem(e,e),t.removeItem(e),t}catch(t){return{}}}function qt(){var t=new Io({debug:!1});return t.hassId=Oo++,t}function Ft(t,e,n){Object.keys(n).forEach((function(r){var i=n[r];if("register"in i&&i.register(e),"getters"in i&&Object.defineProperty(t,r+"Getters",{value:i.getters,enumerable:!0}),"actions"in i){var o={};Object.getOwnPropertyNames(i.actions).forEach((function(t){"function"==typeof i.actions[t]&&Object.defineProperty(o,t,{value:i.actions[t].bind(null,e),enumerable:!0})})),Object.defineProperty(t,r+"Actions",{value:o,enumerable:!0})}}))}function Gt(t,e){return wo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function Kt(t){return Ge(t,"GET","error_log")}function Bt(t,e){var n=e.date;return n.toISOString()}function Yt(){return Lo.getInitialState()}function Jt(t,e){var n=e.date,r=e.entries;return t.set(n,xo(r.map(Uo.fromJSON)))}function Wt(){return Vo.getInitialState()}function Xt(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function Qt(){return Go.getInitialState()}function Zt(t,e){t.dispatch(zo.LOGBOOK_DATE_SELECTED,{date:e})}function $t(t,e){t.dispatch(zo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),Ge(t,"GET","logbook/"+e).then((function(n){return t.dispatch(zo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(zo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function te(t){return!t||(new Date).getTime()-t>Yo}function ee(t){t.registerStores({currentLogbookDate:Lo,isLoadingLogbookEntries:ko,logbookEntries:Vo,logbookEntriesUpdated:Go})}function ne(t){return t.set("active",!0)}function re(t){return t.set("active",!1)}function ie(){return ou.getInitialState()}function oe(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",Ge(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(nu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),An.createNotification(t,n),!1}))}function ue(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return Ge(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(nu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),An.createNotification(t,n),!1}))}function ae(t){t.registerStores({pushNotifications:ou})}function se(t,e){return Ge(t,"POST","template",{template:e})}function ce(t){return t.set("isListening",!0)}function fe(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function he(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function le(){return Eu.getInitialState()}function pe(){return Eu.getInitialState()}function _e(){return Eu.getInitialState()}function de(t){return bu[t.hassId]}function ve(t){var e=de(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(gu.VOICE_TRANSMITTING,{finalTranscript:n}),xn.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(gu.VOICE_DONE)}),(function(){t.dispatch(gu.VOICE_ERROR)}))}}function ye(t){var e=de(t);e&&(e.recognition.stop(),bu[t.hassId]=!1)}function ge(t){ve(t),ye(t)}function me(t){var e=ge.bind(null,t);e();var n=new webkitSpeechRecognition;bu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(gu.VOICE_START)},n.onerror=function(){return t.dispatch(gu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=de(t);if(n){for(var r="",i="",o=e.resultIndex;o>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?_(t)+e:e}function v(){return!0}function y(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function g(t,e){return S(t,e,0)}function m(t,e){return S(t,e,e)}function S(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[On]);if("function"==typeof e)return e}function C(t){return t&&"number"==typeof t.length}function D(t){return null===t||void 0===t?U():o(t)?t.toSeq():V(t)}function R(t){return null===t||void 0===t?U().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():H(t)}function z(t){return null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function k(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function P(t){return!(!t||!t[Tn])}function U(){return An||(An=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new N(t).fromEntrySeq():O(t)?new k(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return C(t)?new L(t):w(t)?new N(t):O(t)?new k(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?B(e,t,"",{"":t}):Y(t)}function B(t,e,n,r){return Array.isArray(e)?t.call(r,n,z(e).map((function(n,r){return B(t,n,r,e)}))):J(e)?t.call(r,n,R(e).map((function(n,r){return B(t,n,r,e)}))):e}function Y(t){return Array.isArray(t)?z(t).map(Y).toList():J(t)?R(t).map(Y).toMap():t}function J(t){return t&&(t.constructor===Object||void 0===t.constructor)}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!s(t);if(c(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var h=!0,l=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,yn)):!W(t.get(r,yn),e))return h=!1,!1}));return h&&t.size===l}function Q(t,e){if(!(this instanceof Q))return new Q(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Cn)return Cn;Cn=this}}function Z(t,e){if(!t)throw new Error(e)}function $(t,e,n){if(!(this instanceof $))return new $(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>Pn?ut(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return st(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=xn[t];return void 0===e&&(e=at(t),Hn===Un&&(Hn=0,xn={}),Hn++,xn[t]=e),e}function at(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ht(t){return null===t||void 0===t?bt():lt(t)&&!c(t)?t:bt().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function lt(t){return!(!t||!t[Vn])}function pt(t,e){this.ownerID=t,this.entries=e}function _t(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function dt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&St(t._root)}function mt(t,e){return b(t,e[0],e[1])}function St(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(qn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function bt(){return Fn||(Fn=Et(0))}function It(t,e,n){var r,i;if(t._root){var o=f(gn),u=f(mn);if(r=Ot(t._root,t.__ownerID,0,void 0,e,n,o,u),!u.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new pt(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):bt()}function Ot(t,e,n,r,i,o,u,a){return t?t.update(e,n,r,i,o,u,a):o===yn?t:(h(a),h(u),new yt(e,r,[i,o]))}function wt(t){return t.constructor===yt||t.constructor===vt}function Tt(t,e,n,r,i){if(t.keyHash===r)return new vt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&vn,a=(0===n?r:r>>>n)&vn,s=u===a?[Tt(t,e,n+_n,r,i)]:(o=new yt(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new dt(t,o+1,u)}function Rt(t,e,r){for(var i=[],u=0;u>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function Nt(t,e,n,r){var i=r?t:p(t);return i[e]=n,i}function Pt(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&io?0:o-n,c=u-n;return c>dn&&(c=dn),function(){if(i===c)return Xn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>dn&&(f=dn),function(){for(;;){if(a){var t=a();if(t!==Xn)return t;a=null}if(c===f)return Xn;var o=e?--f:c++;a=n(s&&s[o],r-_n,i+(o<=t.size||e<0)return t.withMutations((function(t){e<0?Wt(t,e).set(0,n):Wt(t,0,e+1).set(e,n)}));e+=t._origin;var r=t._tail,i=t._root,o=f(mn);return e>=Qt(t._capacity)?r=Bt(r,t.__ownerID,0,e,n,o):i=Bt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):Ft(t._origin,t._capacity,t._level,i,r):t}function Bt(t,e,n,r,i,o){var u=r>>>n&vn,a=t&&u0){var c=t&&t.array[u],f=Bt(c,e,n-_n,r,i,o);return f===c?t:(s=Yt(t,e),s.array[u]=f,s)}return a&&t.array[u]===i?t:(h(o),s=Yt(t,e),void 0===i&&u===s.array.length-1?s.array.pop():s.array[u]=i,s)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Vt(t?t.array.slice():[],e)}function Jt(t,e){if(e>=Qt(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&vn],r-=_n;return n}}function Wt(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var r=t.__ownerID||new l,i=t._origin,o=t._capacity,u=i+e,a=void 0===n?o:n<0?o+n:i+n;if(u===i&&a===o)return t;if(u>=a)return t.clear();for(var s=t._level,c=t._root,f=0;u+f<0;)c=new Vt(c&&c.array.length?[void 0,c]:[],r),s+=_n,f+=1<=1<h?new Vt([],r):_;if(_&&p>h&&u_n;y-=_n){var g=h>>>y&vn;v=v.array[g]=Yt(v.array[g],r)}v.array[h>>>_n&vn]=_}if(a=p)u-=p,a-=p,s=_n,c=null,d=d&&d.removeBefore(r,0,u);else if(u>i||p>>s&vn;if(m!==p>>>s&vn)break;m&&(f+=(1<i&&(c=c.removeBefore(r,s,u-f)),c&&pu&&(u=c.size),o(s)||(c=c.map((function(t){return K(t)}))),i.push(c)}return u>t.size&&(t=t.setSize(u)),Lt(t,e,i)}function Qt(t){return t>>_n<<_n}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function $t(t){return lt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Qn||(Qn=te(bt(),Gt()))}function ne(t,e,n){var r,i,o=t._map,u=t._list,a=o.get(e),s=void 0!==a;if(n===yn){if(!s)return t;u.size>=dn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function ae(t){var e=Ce(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=De,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?Sn:En,n)},e}function se(t,e,n){var r=Ce(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function ce(t,e){var n=Ce(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=De,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function fe(t,e,n,r){var i=Ce(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function he(t,e,n){var r=ht().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function le(t,e,n){var r=u(t),i=(c(t)?Zt():ht()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Ae(t);return i.map((function(e){return Oe(t,o(e))}))}function pe(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n===1/0?n=i:n|=0),y(e,n,i))return t;var o=g(e,i),u=m(n,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,n,r);var a,s=u-o;s===s&&(a=s<0?0:s);var c=Ce(t);return c.size=0===a?a:t.size&&a||void 0,!r&&P(t)&&a>=0&&(c.get=function(e,n){return e=d(this,e),e>=0&&ea)return I();var t=i.next();return r||e===En?t:e===Sn?b(e,s-1,void 0,t):b(e,s-1,t.value[1],t)})},c}function _e(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function de(t,e,n,r){var i=Ce(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===Sn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function ve(t,e){var r=u(t),i=[t].concat(e).map((function(t){return o(t)?r&&(t=n(t)):t=r?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===i.length)return t;if(1===i.length){var s=i[0];if(s===t||r&&u(s)||a(t)&&a(s))return s}var c=new L(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),c}function ye(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){function u(t,c){var f=this;t.__iterate((function(t,i){return(!e||c0}function Ie(t,n,r){var i=Ce(t);return i.size=new L(r).map((function(t){return t.size})).min(),i.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(En,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},i.__iteratorUncached=function(t,i){var o=r.map((function(t){return t=e(t),T(i?t.reverse():t)})),u=0,a=!1;return new E(function(){var e;return a||(e=o.map((function(t){return t.next()})),a=e.some((function(t){return t.done}))),a?I():b(t,u++,n.apply(null,e.map((function(t){return t.value}))))})},i}function Oe(t,e){return P(t)?e:t.constructor(e)}function we(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Te(t){return ft(t.size),_(t)}function Ae(t){return u(t)?n:a(t)?r:i}function Ce(t){return Object.create((u(t)?R:a(t)?z:M).prototype)}function De(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):D.prototype.cacheResult.call(this)}function Re(t,e){return t>e?1:te?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=u(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0});return un(i,r)}function un(t,e){return e=zn(e,3432918353),e=zn(e<<15|e>>>-15,461845907),e=zn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=zn(e^e>>>16,2246822507),e=zn(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=a,e.isAssociative=s,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",fn="@@__IMMUTABLE_KEYED__@@",hn="@@__IMMUTABLE_INDEXED__@@",ln="@@__IMMUTABLE_ORDERED__@@",pn="delete",_n=5,dn=1<<_n,vn=dn-1,yn={},gn={value:!1},mn={value:!1},Sn=0,En=1,bn=2,In="function"==typeof Symbol&&Symbol.iterator,On="@@iterator",wn=In||On;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=Sn,E.VALUES=En,E.ENTRIES=bn,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(D,e),D.of=function(){return D(arguments)},D.prototype.toSeq=function(){return this},D.prototype.toString=function(){return this.__toString("Seq {","}")},D.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},D.prototype.__iterate=function(t,e){return F(this,t,e,!0)},D.prototype.__iterator=function(t,e){return G(this,t,e,!0)},t(R,D),R.prototype.toKeyedSeq=function(){return this},t(z,D),z.of=function(){return z(arguments)},z.prototype.toIndexedSeq=function(){return this},z.prototype.toString=function(){return this.__toString("Seq [","]")},z.prototype.__iterate=function(t,e){return F(this,t,e,!1)},z.prototype.__iterator=function(t,e){return G(this,t,e,!1)},t(M,D),M.of=function(){return M(arguments)},M.prototype.toSetSeq=function(){return this},D.isSeq=P,D.Keyed=R,D.Set=M,D.Indexed=z;var Tn="@@__IMMUTABLE_SEQ__@@";D.prototype[Tn]=!0,t(L,z),L.prototype.get=function(t,e){return this.has(t)?this._array[d(this,t)]:e},L.prototype.__iterate=function(t,e){for(var n=this,r=this._array,i=r.length-1,o=0;o<=i;o++)if(t(r[e?i-o:o],o,n)===!1)return o+1;return o},L.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(j,R),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},j.prototype[ln]=!0,t(k,z),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},k.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,z),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(Q,z),Q.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Q.prototype.get=function(t,e){return this.has(t)?this._value:e},Q.prototype.includes=function(t){return W(this._value,t)},Q.prototype.slice=function(t,e){var n=this.size;return y(t,e,n)?this:new Q(this._value,m(e,n)-g(t,n))},Q.prototype.reverse=function(){return this},Q.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Q.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Q.prototype.__iterate=function(t,e){for(var n=this,r=0;r=0&&e=0&&nn?I():b(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:X(this,t)};var Dn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var Rn,zn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Mn=Object.isExtensible,Ln=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),jn="function"==typeof WeakMap;jn&&(Rn=new WeakMap);var kn=0,Nn="__immutablehash__";"function"==typeof Symbol&&(Nn=Symbol(Nn));var Pn=16,Un=255,Hn=0,xn={};t(ht,et),ht.of=function(){var t=sn.call(arguments,0);return bt().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},ht.prototype.toString=function(){return this.__toString("Map {","}")},ht.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ht.prototype.set=function(t,e){return It(this,t,e)},ht.prototype.setIn=function(t,e){return this.updateIn(t,yn,(function(){return e}))},ht.prototype.remove=function(t){return It(this,t,yn)},ht.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return yn}))},ht.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},ht.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=jt(this,ze(t),e,n);return r===yn?void 0:r},ht.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):bt()},ht.prototype.merge=function(){return Rt(this,void 0,arguments)},ht.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return Rt(this,t,e)},ht.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},ht.prototype.mergeDeep=function(){return Rt(this,zt,arguments)},ht.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return Rt(this,Mt(t),e)},ht.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},ht.prototype.sort=function(t){return Zt(Se(this,t))},ht.prototype.sortBy=function(t,e){return Zt(Se(this,e,t))},ht.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ht.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new l)},ht.prototype.asImmutable=function(){return this.__ensureOwner()},ht.prototype.wasAltered=function(){return this.__altered},ht.prototype.__iterator=function(t,e){return new gt(this,t,e)},ht.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},ht.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ht.isMap=lt;var Vn="@@__IMMUTABLE_MAP__@@",qn=ht.prototype;qn[Vn]=!0,qn[pn]=qn.remove,qn.removeIn=qn.deleteIn,pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Gn)return At(t,s,r,i);var _=t&&t===this.ownerID,d=_?s:p(s);return l?a?c===f-1?d.pop():d[c]=d.pop():d[c]=[r,i]:d.push([r,i]),_?(this.entries=d,this):new pt(t,d)}},_t.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&vn),o=this.bitmap;return 0===(o&i)?r:this.nodes[kt(o&i-1)].get(t+_n,e,n,r)},_t.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=1<=Kn)return Dt(t,l,c,a,_);if(f&&!_&&2===l.length&&wt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&wt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?Nt(l,h,_,d):Ut(l,h,d):Pt(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new _t(t,v,y)},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&vn,o=this.nodes[i];return o?o.get(t+_n,e,n,r):r},dt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=i===yn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Ot(f,t,e+_n,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&vn;if(r>=this.array.length)return new Vt([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-_n,n),i===u&&o)return this}if(o&&!i)return this;var a=Yt(this,t);if(!o)for(var s=0;s>>e&vn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-_n,n),i===o&&r===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Wn,Xn={};t(Zt,ht),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=$t,Zt.prototype[ln]=!0,Zt.prototype[pn]=Zt.prototype.remove;var Qn;t(re,R),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=se(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Te(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Te(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},re.prototype[ln]=!0,t(ie,z),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(oe,M),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ue,R),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){we(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ue.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){we(r);var i=o(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=De,t(Me,et),Me.prototype.toString=function(){return this.__toString(je(this)+" {","}")},Me.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Me.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Me.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Le(this,bt()))},Me.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+je(this));if(this._map&&!this._map.has(t)){var n=this._defaultValues[t];if(e===n)return this}var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:Le(this,r)},Me.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Le(this,e)},Me.prototype.wasAltered=function(){return this._map.wasAltered()},Me.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Me.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Me.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Le(this,e,t):(this.__ownerID=t,this._map=e,this)};var Zn=Me.prototype;Zn[pn]=Zn.remove,Zn.deleteIn=Zn.removeIn=qn.removeIn,Zn.merge=qn.merge,Zn.mergeWith=qn.mergeWith,Zn.mergeIn=qn.mergeIn,Zn.mergeDeep=qn.mergeDeep,Zn.mergeDeepWith=qn.mergeDeepWith,Zn.mergeDeepIn=qn.mergeDeepIn,Zn.setIn=qn.setIn,Zn.update=qn.update,Zn.updateIn=qn.updateIn,Zn.withMutations=qn.withMutations,Zn.asMutable=qn.asMutable,Zn.asImmutable=qn.asImmutable,t(Pe,rt),Pe.of=function(){return this(arguments)},Pe.fromKeys=function(t){return this(n(t).keySeq())},Pe.prototype.toString=function(){return this.__toString("Set {","}")},Pe.prototype.has=function(t){return this._map.has(t)},Pe.prototype.add=function(t){return He(this,this._map.set(t,!0))},Pe.prototype.remove=function(t){return He(this,this._map.remove(t))},Pe.prototype.clear=function(){return He(this,this._map.clear())},Pe.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ft(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pop=function(){return this.slice(1)},Be.prototype.unshift=function(){return this.push.apply(this,arguments)},Be.prototype.unshiftAll=function(t){return this.pushAll(t)},Be.prototype.shift=function(){return this.pop.apply(this,arguments)},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):We()},Be.prototype.slice=function(t,e){if(y(t,e,this.size))return this;var n=g(t,this.size),r=m(e,this.size);if(r!==this.size)return nt.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Je(i,o)},Be.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Je(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Be.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},Be.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Be.isStack=Ye;var ir="@@__IMMUTABLE_STACK__@@",or=Be.prototype;or[ir]=!0,or.withMutations=qn.withMutations,or.asMutable=qn.asMutable,or.asImmutable=qn.asImmutable,or.wasAltered=qn.wasAltered;var ur;e.Iterator=E,Xe(e,{toArray:function(){ft(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,n){t[n]=e})),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return ht(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate((function(e,n){t[n]=e})),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return qe(u(this)?this.valueSeq():this)},toSet:function(){return Pe(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Be(u(this)?this.valueSeq():this)},toList:function(){return Ht(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sn.call(arguments,0);return Oe(this,ve(this,t))},includes:function(t){return this.some((function(e){return W(e,t)}))},entries:function(){return this.__iterator(bn)},every:function(t,e){ft(this.size);var n=!0;return this.__iterate((function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1})),n},filter:function(t,e){return Oe(this,fe(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate((function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""})),e},keys:function(){return this.__iterator(Sn)},map:function(t,e){return Oe(this,se(this,t,e))},reduce:function(t,e,n){ft(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate((function(e,o,u){i?(i=!1,r=e):r=t.call(n,r,e,o,u)})),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Oe(this,ce(this,!0))},slice:function(t,e){return Oe(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return Oe(this,Se(this,t))},values:function(){return this.__iterator(En)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return _(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return he(this,t,e)},equals:function(t){return X(this,t)},entrySeq:function(){var t=this;if(t._cache)return new L(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter($e(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate((function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1})),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(v)},flatMap:function(t,e){return Oe(this,ge(this,t,e))},flatten:function(t){return Oe(this,ye(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find((function(e,n){return W(n,t)}),void 0,e)},getIn:function(t,e){for(var n,r=this,i=ze(t);!(n=i.next()).done;){var o=n.value;if(r=r&&r.get?r.get(o,yn):yn,r===yn)return e}return r},groupBy:function(t,e){return le(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey((function(e){return W(e,t)}))},keySeq:function(){return this.toSeq().map(Qe).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return Oe(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return Oe(this,de(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return Oe(this,Se(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return Oe(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return Oe(this,_e(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var ar=e.prototype;ar[cn]=!0,ar[wn]=ar.values,ar.__toJS=ar.toArray,ar.__toStringMapper=en,ar.inspect=ar.toSource=function(){return this.toString()},ar.chain=ar.flatMap,ar.contains=ar.includes,Xe(n,{flip:function(){return Oe(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return Oe(this,this.toSeq().map((function(i,o){return t.call(e,[o,i],r++,n)})).fromEntrySeq())},mapKeys:function(t,e){var n=this;return Oe(this,this.toSeq().flip().map((function(r,i){return t.call(e,r,i,n)})).flip())}});var sr=n.prototype;sr[fn]=!0,sr[wn]=ar.entries,sr.__toJS=ar.toObject,sr.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Xe(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return Oe(this,fe(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return Oe(this,ce(this,!1))},slice:function(t,e){return Oe(this,pe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=g(t,t<0?this.count():this.size);var r=this.slice(0,t);return Oe(this,1===n?r:r.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return Oe(this,ye(this,t,!1))},get:function(t,e){return t=d(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=d(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,m.toFactory)(E),t.exports=e.default},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new C({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return S(t,[n])}))})),m(t)}))}function u(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){var r=t.get("logger");if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var i=t.get("state"),o=t.get("dirtyStores"),u=i.withMutations((function(u){r.dispatchStart(t,e,n),t.get("stores").forEach((function(i,a){var s=u.get(a),c=void 0;try{c=i.handle(s,e,n)}catch(e){throw r.dispatchError(t,e.message),e}if(void 0===c&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw r.dispatchError(t,h),new Error(h)}u.set(a,c),s!==c&&(o=o.add(a))})),r.dispatchEnd(t,u,o,i)})),a=t.set("state",u).set("dirtyStores",o).update("storeStates",(function(t){return S(t,o)}));return m(a)}function s(t,e){var n=[],r=(0,O.toImmutable)({}).withMutations((function(r){(0,A.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=b.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return S(t,n)}))}function c(t,e,n){var r=e;(0,T.isKeyPath)(e)&&(e=(0,w.fromKeyPath)(e));var i=t.get("nextId"),o=(0,w.getStoreDeps)(e),u=b.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,b.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,T.isKeyPath)(e)&&(0,T.isKeyPath)(r)?(0,T.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return S(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,T.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,w.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");var r=t.get("cache"),o=r.lookup(e),u=!o||y(t,o);return u&&(o=g(t,e)),i(o.get("value"),t.update("cache",(function(t){return u?t.miss(e,o):t.hit(e)})))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",b.default.Set())}function y(t,e){var n=e.get("storeStates");return!n.size||n.some((function(e,n){return t.getIn(["storeStates",n])!==e}))}function g(t,e){var n=(0,w.getDeps)(e).map((function(e){return _(t,e).result})),r=(0,w.getComputeFn)(e).apply(null,n),i=(0,w.getStoreDeps)(e),o=(0,O.toImmutable)({}).withMutations((function(e){i.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return(0,I.CacheEntry)({value:r,storeStates:o,dispatchId:t.get("dispatchId")})}function m(t){return t.update("dispatchId",(function(t){return t+1}))}function S(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var E=n(3),b=r(E),I=n(9),O=n(5),w=n(10),T=n(11),A=n(4),C=b.default.Record({result:null,reactorState:null})},function(t,e,n){function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){return new s}Object.defineProperty(e,"__esModule",{value:!0});var o=(function(){function t(t,e){for(var n=0;nn.dispatchId)throw new Error("Refusing to cache older value");return n})))}},{key:"evict",value:function(e){return new t(this.cache.remove(e))}}]),t})();e.BasicCache=s;var c=1e3,f=1,h=(function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1],i=arguments.length<=2||void 0===arguments[2]?new s:arguments[2],o=arguments.length<=3||void 0===arguments[3]?(0,u.OrderedSet)():arguments[3];r(this,t),console.log("using LRU"),this.limit=e,this.evictCount=n,this.cache=i,this.lru=o}return o(t,[{key:"lookup",value:function(t,e){return this.cache.lookup(t,e)}},{key:"has",value:function(t){return this.cache.has(t)}},{key:"asMap",value:function(){return this.cache.asMap()}},{key:"hit",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache,this.lru.remove(e).add(e)):this}},{key:"miss",value:function(e,n){var r;if(this.lru.size>=this.limit){if(this.has(e))return new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.remove(e).add(e));var i=this.lru.take(this.evictCount).reduce((function(t,e){return t.evict(e)}),this.cache).miss(e,n);r=new t(this.limit,this.evictCount,i,this.lru.skip(this.evictCount).add(e))}else r=new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.add(e));return r}},{key:"evict",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache.evict(e),this.lru.remove(e)):this}}]),t})();e.LRUCache=h},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(8),i={dispatchStart:function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},dispatchError:function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},dispatchEnd:function(t,e,n,i){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}};e.ConsoleGroupLogger=i;var o={dispatchStart:function(t,e,n){},dispatchError:function(t,e){},dispatchEnd:function(t,e,n){}};e.NoopLogger=o},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n(9),o=n(12),u=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=u;var a=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=a;var s=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,i.DefaultCache)(),logger:o.NoopLogger,storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:u});e.ReactorState=s;var c=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=c}])}))})),Ie=t(be),Oe=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},we=Oe,Te=we({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),Ae=Ie.Store,Ce=Ie.toImmutable,De=new Ae({getInitialState:function(){return Ce({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Te.VALIDATING_AUTH_TOKEN,n),this.on(Te.VALID_AUTH_TOKEN,r),this.on(Te.INVALID_AUTH_TOKEN,i)}}),Re=Ie.Store,ze=Ie.toImmutable,Me=new Re({getInitialState:function(){return ze({authToken:null,host:""})},initialize:function(){this.on(Te.VALID_AUTH_TOKEN,o),this.on(Te.LOG_OUT,u)}}),Le=Ie.Store,je=new Le({getInitialState:function(){return!0},initialize:function(){this.on(Te.VALID_AUTH_TOKEN,a)}}),ke=we({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Ne=Ie.Store,Pe=Ie.toImmutable,Ue=new Ne({getInitialState:function(){return Pe({isStreaming:!1,hasError:!1})},initialize:function(){this.on(ke.STREAM_START,s),this.on(ke.STREAM_ERROR,c),this.on(ke.LOG_OUT,f)}}),He=e((function(t,e){function n(t){return{type:"auth",api_password:t}}function r(){return{type:"get_states"}}function i(){return{type:"get_config"}}function o(){return{type:"get_services"}}function u(){return{type:"get_panels"}}function a(t,e,n){var r={type:"call_service",domain:t,service:e};return n&&(r.service_data=n),r}function s(t){var e={type:"subscribe_events"};return t&&(e.event_type=t),e}function c(t){return{type:"unsubscribe_events",subscription:t}}function f(){return{type:"ping"}}function h(t){return t.result}function l(t,e){var n=new d(t,e);return n.connect()}Object.defineProperty(e,"__esModule",{value:!0});var p=1,_=2,d=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};d.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},d.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},d.prototype.connect=function(){var t=this;return new Promise(function(e,r){var i=t.commands;Object.keys(i).forEach((function(t){var e=i[t];e.reject&&e.reject()}));var o=!1;t.connectionTries+=1,t.socket=new WebSocket(t.url),t.socket.addEventListener("open",(function(){t.connectionTries=0})),t.socket.addEventListener("message",(function(u){var a=JSON.parse(u.data);switch(console.log("Received",a),a.type){case"event":t.commands[a.id].eventCallback(a.event);
+break;case"result":a.success?t.commands[a.id].resolve(a):t.commands[a.id].reject(a.error),delete t.commands[a.id];break;case"pong":break;case"auth_required":t.sendMessage(n(t.options.authToken));break;case"auth_invalid":r({code:_}),o=!0;break;case"auth_ok":e(t),t.fireEvent("ready"),t.commandId=1,t.commands={},Object.keys(i).forEach((function(e){var n=i[e];n.eventType&&t.subscribeEvents(n.eventCallback,n.eventType).then((function(t){n.unsubscribe=t}))}));break;default:console.warn("Unhandled message",a)}})),t.socket.addEventListener("close",(function(){if(!o&&!t.closeRequested){0===t.connectionTries?t.fireEvent("disconnected"):r(p);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},d.prototype.close=function(){this.closeRequested=!0,this.socket.close()},d.prototype.getStates=function(){return this.sendMessagePromise(r()).then(h)},d.prototype.getServices=function(){return this.sendMessagePromise(o()).then(h)},d.prototype.getPanels=function(){return this.sendMessagePromise(u()).then(h)},d.prototype.getConfig=function(){return this.sendMessagePromise(i()).then(h)},d.prototype.callService=function(t,e,n){return this.sendMessagePromise(a(t,e,n))},d.prototype.subscribeEvents=function(t,e){var n=this;return this.sendMessagePromise(s(e)).then((function(r){var i={eventCallback:t,eventType:e,unsubscribe:function(){return n.sendMessagePromise(c(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},d.prototype.ping=function(){return this.sendMessagePromise(f())},d.prototype.sendMessage=function(t){console.log("Sending",t),this.socket.send(JSON.stringify(t))},d.prototype.sendMessagePromise=function(t){var e=this;return new Promise(function(n,r){e.commandId+=1;var i=e.commandId;t.id=i,e.commands[i]={resolve:n,reject:r},e.sendMessage(t)})},e.ERR_CANNOT_CONNECT=p,e.ERR_INVALID_AUTH=_,e.createConnection=l,e.default=l})),xe=He.createConnection,Ve=we({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),qe=Ie.Store,Fe=new qe({getInitialState:function(){return!0},initialize:function(){this.on(Ve.API_FETCH_ALL_START,(function(){return!0})),this.on(Ve.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(Ve.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(Ve.LOG_OUT,(function(){return!1}))}}),Ge=h,Ke=we({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),Be=Ie.Store,Ye=Ie.toImmutable,Je=new Be({getInitialState:function(){return Ye({})},initialize:function(){var t=this;this.on(Ke.API_FETCH_SUCCESS,l),this.on(Ke.API_SAVE_SUCCESS,l),this.on(Ke.API_DELETE_SUCCESS,p),this.on(Ke.LOG_OUT,(function(){return t.getInitialState()}))}}),We=Object.prototype.hasOwnProperty,Xe=Object.prototype.propertyIsEnumerable,Qe=d()?Object.assign:function(t,e){for(var n,r,i=arguments,o=_(t),u=1;uhF0^$nd?|=1-3_eji{x
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

>OtB$S?6nfc+;9OF)(sdw)zwNXm;txfb5 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 diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index c6c2fed44be..cfa9b33f497 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,5 +1,5 @@

\ No newline at end of file +},customStyle:null,getComputedStyleValue:function(e){return!i&&this._styleProperties&&this._styleProperties[e]||getComputedStyle(this).getPropertyValue(e)},_setupStyleProperties:function(){this.customStyle={},this._styleCache=null,this._styleProperties=null,this._scopeSelector=null,this._ownStyleProperties=null,this._customStyle=null},_needsStyleProperties:function(){return Boolean(!i&&this._ownStylePropertyNames&&this._ownStylePropertyNames.length)},_validateApplyShim:function(){if(this.__applyShimInvalid){Polymer.ApplyShim.transform(this._styles,this.__proto__);var e=n.elementStyles(this);if(s){var t=this._template.content.querySelector("style");t&&(t.textContent=e)}else{var r=this._scopeStyle&&this._scopeStyle.nextSibling;r&&(r.textContent=e)}}},_beforeAttached:function(){this._scopeSelector&&!this.__stylePropertiesInvalid||!this._needsStyleProperties()||(this.__stylePropertiesInvalid=!1,this._updateStyleProperties())},_findStyleHost:function(){for(var e,t=this;e=Polymer.dom(t).getOwnerRoot();){if(Polymer.isInstance(e.host))return e.host;t=e.host}return r},_updateStyleProperties:function(){var e,n=this._findStyleHost();n._styleProperties||n._computeStyleProperties(),n._styleCache||(n._styleCache=new Polymer.StyleCache);var r=t.propertyDataFromStyles(n._styles,this),i=!this.__notStyleScopeCacheable;i&&(r.key.customStyle=this.customStyle,e=n._styleCache.retrieve(this.is,r.key,this._styles));var a=Boolean(e);a?this._styleProperties=e._styleProperties:this._computeStyleProperties(r.properties),this._computeOwnStyleProperties(),a||(e=o.retrieve(this.is,this._ownStyleProperties,this._styles));var l=Boolean(e)&&!a,c=this._applyStyleProperties(e);a||(c=c&&s?c.cloneNode(!0):c,e={style:c,_scopeSelector:this._scopeSelector,_styleProperties:this._styleProperties},i&&(r.key.customStyle={},this.mixin(r.key.customStyle,this.customStyle),n._styleCache.store(this.is,e,r.key,this._styles)),l||o.store(this.is,Object.create(e),this._ownStyleProperties,this._styles))},_computeStyleProperties:function(e){var n=this._findStyleHost();n._styleProperties||n._computeStyleProperties();var r=Object.create(n._styleProperties),s=t.hostAndRootPropertiesForScope(this);this.mixin(r,s.hostProps),e=e||t.propertyDataFromStyles(n._styles,this).properties,this.mixin(r,e),this.mixin(r,s.rootProps),t.mixinCustomStyle(r,this.customStyle),t.reify(r),this._styleProperties=r},_computeOwnStyleProperties:function(){for(var e,t={},n=0;n0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var c=a[i];void 0!==c&&this._detachAndRemoveInstance(c)}var h=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return h._filterFn(r.getItem(e))})),l.sort(function(e,t){return h._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 975f1668bd41f653de649b6f07bd65e86202fd1e..76d65cc903b46fe3bbdba06c91b97b1c2cc3a06f 100644 GIT binary patch delta 45458 zcmbR9oBibvc6Rx04vyMBt48*%?2PLK>(|QizcUs!-gBOf`DXBh;%^!HHq)%?-hK2{ zOo*MM;bJv=q2ZLZpG=?fPwNqz`SE|j4;I-o6?eVMH%!T8+wxrG@I~X=?g^hmw`r_< zeRA@}`L3r@EY9gy7cA>`?GIeAx8{`l8Ux*cz^ASU%D)-^zWdnnpZ*i>o1Z;@-!0n7 z)1Bs1|52uYoq0{j_o5w6hO5{VEVC`Eotx~KxBnAe_VicWvZ=wHmU~XGd$owq)Wk;n zT<@_unZGwRUF8`=G*l-h-{HKiynd%)a&~kZLse(dUn-G!+ia3mh*2C!dkTi#V>y}{C8BltvxI3|I&zpGdenxzO0zK z#J%|Gv62VE2j(#>*R9X}!>j#m@2lg-r<64P5)Fv->bY|>O8NF;l~;3K$;fxJ?B-ps z+Qj#!XyRwn#&3_+!(LY3oAb;s!d(4+Bcu2dpT&=}wmuJ9{r2Xu7qX$@QP;M0wQY~k zT0ZCKs_ZBSFAW=smD4wCJh}Ne(q-*82J=I@zWbH0cC?7Up5%VF@a^-0+WPYCn@;^d zd+qSn6-*j>YGPdcui)v7f1u3c|ZMo$J=M z7`}b7+tuv<>x_l`yJAl*;9K~bZT}*su};XSutvx_PbP(RW){eB`|8 zWhv>+^q)cX&OW0un+3hL|F)H`K6m~X|8y-8MlM5(8OD2V-wvz%@?w4Z&&g*$$W(T} zT^c9TCs(*~_BN+w-8DMq6-@^-Zf#pH`uNf-!KZ%HQ$-kc>+iSS`sOz$fH&GOezDdm zlYps;zm`duUSV}#InjQu!h<-Td+R3DTkLt58u@hn_0yjwY+7P}aXQPRLu&-wO;Z-s z?-iK$+-k#$2fa^ETx;Zx-E@MhBH`|wYaeduug-AF)F}!6qg(Q8wtKMS%9Ae4>AP2a zwyV|sbpEJ~y{i8Ao#qU`>Q}A5{wRFgsa2;hTe6#4wPuUyJ?%VLk+g1F&V9)bUw>Ve zy1RDqe4DsEktQ8#kG9Y273UE4?OJ#J=JPcP_S<$f-gske^Sn>d{ZQ^=Q>7yZikB>A zKfT87y=esZl8sTy}rQ&ZLib)~yJXv}EMH%%>SWjPFx}E*n zqqz0m&$h(Oi#xQMJ51bFG^zEfko0zsiuD!EaUU+bEBrRIzu@$kA^+yC{k`+2dy6s3 z*01~a!EZ_8)e^?GcLrZ>IY0fNAGb23NzU&C*VOOJ!q4xseP2Ft*5}(Vxu$luhq7wl z-CE^yGe${EW7E~XiN{kuGEBU#eeLtruS(_7ZnbF)`x!-76;5PfO`Xv8_i>MoO78!t zHU8-?x}D`u1@mL0(m!3hbjVfzk8e#Lt2{osBp9k|^)!%(!dp_aFO}__scd}pX zJNJ+$v*qx&gD-;oZ2WnSrMhh}id0&BHlU|==JW38_|TaWGvmY~x-Xo_b&m*mCNE_q zcQn6vahxY(+Y9#-wkP~o8tPh4*NVQWXMHy0b-l>z1F;%jMHcY}vlpEV+ps|G{?~Sf z(`O#B|8qFTIca*DIHQ`1L?Wwv=8{YHap#{Gs0N4inf18c)83)Q$PnJ^G`XQ9XU)1M zvFRJc8ATbxrXLe$)UE%X@hnbp@{vD$h4~e?5<5?2Ox~iP(b!e?vDAIN+MKU~uXuEJ zEvh!VWEL}XX1jh)_M~IXoY#(QDtg{Nee*S4u|15-&zs29q^~?!t^DtAzZLK5AM1?S z`~CMEsL@@;e)g`^r_|*uSEzXZ&QR^i>Rgi3!xdkSZX&+vD%|l(KExWz@x5v4i)BUnrU*Uh3p~e53SxaaBeOf9kCXw5=?sU-OLkD!! z6}HvSeG=bzVBN!S(hP?C+ngn|h4s@IbvM15EY#nj)B8<+kpPQf+4|p17b5mBzy25# z6QZ@Z=+&PF&!ekcggpN2)EE1*!uwi%c5#hHjqt6TUX7bd%vIXnb2MaMGd#GVUF%Oc zvyl0uEfXV_D*oRw?Zlht5T9+Vdf$0BI^B|5yYNhVQD3Z~z2&(zYU|!Ge+}-=>+bsb zcSrE0W7B5uefVN|l{Z6Ru{m2ql3r1Tgnp05qWirDB`c(7Y~CBSX8HLnqeN5TEt{se z%&!+I>R0gOIN?5P$L1rqPh}Nfkv_cBb@thbscU{Z-;pf45;Jp2@lB_Xp5^OvrFycY zZ|2|T^4suu7uULP@1&hM-`(R84=t|;n z>&e=*cg+tDOf4w)yVc}+f7(Tv1@*eU0X;hHwskMg)n8w6?!epjE1^;cYxjf*X>VIM zy}bYHlYjeCWo3W-oVHZt!sU&#bPxZ2m(%!vcGl_i$i)%$S)g&`IS#QL&F|_yB(&UHCD?nw`#PVr#n#wz^(&nj@n+jyyH}pw_2VLcRbqx@tD2CY z?TGpSJjTSc>ISa#HE82>Qvz(oAa>`O?>D7l; zoXpJ1muO{O_+rZQD`i{xnf>b9U;h#~DP@K< z)7TRICE#12@-LpF-vk;3mkaI;HewY#WtRExrSh|bza?G8EWQq$N7snYES zr(%|^KVvOlTzovf6HX*Ob7uVF_U^Vo=#+K2ZA`sBHydu& zTP?pc$7a6s>dE^)9Y40sLPY9XuLO5`qg<=0Yp<`AI7>@_d8f&;qe-)?SFAGj_ehwR zA9{eZqTr*h&>HOn`^9~;;xba~R{l+!@zQ!hz|^0eS2H3Wmk2wo?K^g%guRO)u4$6- z=kS{AZ;zA(`q(?a;nc3_Z3;c5+~+gh?Z1%OpZX+*9ft!hurH`==X~bH5m)m!@9d{T z4fp3P|NlBP+S>cx^D4W( zNissyswVFExhJkWpKjW{V4+o;fnY)T_3UroV?}3|uaBEsXJchkU-dK4olB%k$KdHf ziK8m3gzJqbD@FP%*GzIPYQ3^By>)t|{5cg-2Yxx@f0mu=u76=(vEeh1-p4;Bfzv|M zrPD0)xN^%Dto*r(<(1jI1jQ?fjVnc8$nFz7TJXvtYLOE6U#6BWL(R2)Lb?xDR;~-$ zm%PrZ~EAsMAEt{VEOFQ-`&v*LJ<`yOO z|HsGv{(rxW4unkGvv{HiW29PS;$pSnC;g)4*QDrxo<@22BWA_O;`G%EZn&%en$~#X1Rl@UO{xU;e@;;C|f` zo4w37KIaPsCo~mIYBKlP?e}y|enn(quXamjy{mgE7k^h{$knuz=kGrucUuvKFPRcb#>9i5rK}mJ?Rm1}o}vbpM^}-M%$- z`HzKCw)cfE#~fj`s`{?AX#JlPYW}T> zxzm$;DMlpdv$~4@x1UOEulB|+I$Zv#dAH=F4xQ_){wHQs&y9Vy`-M(k^12EW#;r9x zb9K9NLg&rD6D4*xgh?@Z!rbZK-|jzm@pOy!Hv`EZI~HU~M!omE^dj`A&N}P2^QSgl z?ySnPJF_-o|AUirOR~6)>YMj9rzUPoo?Y{aX=B{>Rf<|nx+zGpu3`8jjXcG7E)&ENd@%0jyf3Hy|4(_VimEUT^UEReqP&*|v- z*HT|IG!-xXO8D~5Aza1wYj@`p?}vKVu5-P&Iy7zHn-3@3@=IQ)WYy}Z9rLVb+9UX< zJ2&Ut*@C**P=hVE-@2!DRJ-mGi}8L~?(pK@}(S1gHE+kX975`rP*Si{?t-&BDE>iw*fKOfQ)w ze}8pf<3vXD9($JHH(Vd*e|}Xz#Uf)N!yRt>CsiI7{_)qF?Ctxjzq4ugk@_c{*#)&r zRU4lEx*O`ZX~DctcXB?|zYhPfYR^F>J?S6wn%t*o&fHtqlKXf3?I+pm->2rh@$Zmt&(SGslU|glB3x}IH*xyybw%|*Yz(ImTx?KbWzN9k;zig>k|`$v^mzix5-vcD_`xrI%=ox z#5mqkzH2qPC$9@e2U<@SnHXQpDz)-(ljnnq>G7YARv(yN`PAyeB7SSmtM+r2tWJpS zF^>t6y{wRWcY9^*Zofkh+;*I>UG@B_snM3`)$6@)nR7MXjM9j%J(-uK_4(M14u^xU zmn{+abnAP6{0n1ojo4z7H9gaBXyrG(JEwF-^g&We%=8P&jH2}nXE!xAe|eK_vA3X{ z!^=vubaK3<=Lyx)@71gy^+vacn{6GUT#ruR+U8Sax$~ar ztG{`X@2q~$sV;~-EWM@7dj9#(+Tt7Z7bY~mntN`hrR&~|Zze_4^y~IzMzdpV|ElNh56(|z4FB)h{>Qha z-*QWti`9K2_O)NO-+h>QRBihyfzwGFZXAqwRkE`~U6plB@VD;s0w3gEi$6}h*VMM$ z(f9455I=)C&nK1J%+r$)+dY`SlKGD9u;Z%kSh!%AK#{ zvrv^|UwvF`c$`jG%x&$2j_uFin01=0X^S&eo>HW@pr_S^ zy?4P@E0gze=Uba5YKEP-qi1*ZN$K>aKShx{qJCbnl5- z-1WjnxKZQGG{Z-|6+h&5xp)c4o0OOD>}pC~yz%X#V!@Qdop%qYd5BEQ^0(Y@d6(h2 zuT$Lw-5s(5`1a3GJjAHA<^GN3^GhFWm#K3sl9y4r;kHWQ=*x*8=dBK(9sRM({=k_f z$CP&^oac0xR^MJ)8GH8gbdUdj2@mrOz2x==Oy2z3*do?+f%`}8ndz@9{i0$ftgt^Z zbX-1di- z+Q*(gtf=|?{%5Z|gYh3dtDBE%8xOL3)>j0?Z(jG$Pfub+((Z4r0ShOIYW@h73kge@ z?>NtKqrFG!F^xM}$8+npzV%pfynB(?>NM7;yi57cPEMG3f8|G^rrYNQ-rl`^I3l4k z`Ydba`xi}o?fRu{qAEG{?on6ICp|ONOB9gE~E@wLh_5bBXD$ z_qj)T&nKDRj-LKVP4!JfkIm$VjWKq0Nd^BLe#Yb}YyEoHeWd2tIJhnVsPxLt|T(o#CPTboR}QP@HMG$mPV`y8bi$OMN!4 zT=#j!c9rykl5IZrzrQ@b8GK4%n!#fh8Cn+QZBwm#HeY4u75`w~FIJb(l+k`^T?zN3mF_P;IWzUAG-}?M$hnE(q0UtA zcplrP`opR$FCKAk7Ma|=>56!&r^(sKPu^E1J$3brY^z=REZoV}ZvP>cl)^x!e+y2& zo_vsd!Tw6Y89P`p>bTi{$AsI z3m+sDHTvETb84TW$)}qY?_M!C%)|2W%k8V}Y9Cy^bM?TL9ryMs&S1H0yqjPB^nw#j z8h>YZZc~`s#`0tJrd=g5RwWl6sLL7&XRW=!&*0s{c!0Y_Y<2ccsqgc1^WTegr#2hD z2|T%%=W3#F?vqFRf7j0mU;XCP(~VwfJ7jMJ#&0nrYe(X4XG)ld8Jj%~0N5S6zQzY0@XZ z*wY7gZF+D1!S2@9=NI#0FHH6>EeMJGd+=@Y9k0B&jD35zH$;7VCT+D~CX>l$?K@4u zxy846Cbk!{C@?A?-1D%z{Cs`&?@!kkUk;b&``=sl=hwTV-cKA4pXQWjesQbO-QmsW zKjOddNa*xjUi7cvOPlSb@KZ6JTbSNhU8*;<)r)Icqx^5{QWNu?hLIcmPU_vW?bYpl zuv6xw?E%+%y`CE~AjJyQIP5NV+JfiR}^ZJHm_rmj#7~3?3ruq z)phPq{ldynkGjnn_Ip#=c&4(QQCqybtKj8RQM=#5LGuhQeVlDHAE4*FLRc;Sb=Op4#~7UBq1a5Sw(?QDuwgK>Q!pX(F77(=P<0MEn~ZI4># zWgU|0DihO{@|sdvd2$7pLE?UiQ(IDOy57HYi*VCf73*PgZfSss@4h=6S|JD1PMmC* zkyOpTezO2LPC-*%LT~p0?b&=e=XsI&~E-qy;euZ#!i#{r6hjuDnI}u01@zR#@S_FVo{4OM-7(J>1kT-nGq) z*KAwqru>lATPN?j994TE_Ih4X#nMBvbs6)S)?aPA!vE>n)ZQz+2dBt9y?gg<+Af{d zcF#mVxV$rU7C(4Yx8nH@3(vooCcH9>sK|Z!sx|hq+tVJq2{SL7@mGgDsbA51F6`ng z-UG+?o)Dch{btbroXk+JhlkcKYjxYJ_^{WxouleNm9vM{kv%?FXRKMR(fi=zZ2d(k zujfv1)4Fvyocqv=4wuE+)}n$us(!hi`5hmotgflzVy#;`VL``BO=Abfqn`Vg?olY2 zx?A}98?}O+pG_}*=ziG1|EtmBYUhsldMU>%@&fy8CY%tIi*x%wi9z^>vd4C{e`2bs zK?|1b_0vze|3f`;srVw}ALc8!nD3r*+-ySf8vY{>9;+C8cP7XbZ8-VL!B5%m+}x)E z;;Z(tPJhe#gKf?=|H=go2CqYUc1m6GKd-q^YR>V(O;uit+oyz{`h0?I^>XiD=h#kL z_-zxcx3~~+cHOsam&`6aI;FEJag&@u>BFZ=IqTS5_5{eSoXS1P#G}~C-7m}KO~JEe zj4_tEZEBgOSEgLO8(aTda%;c%uQQvYj9;#~d;4;`#Xfbd1(ITOpI6_~aM&gAWrc

`L_jp8wt0^iOPgF@2`uObfm(<(wGfeKS%`C2;DWqmE?3k-2cu*m>zRb8S zLBKzsX?aB6?#uJfwVRb?u!w&=#3t;(Yn`++RLhNre@{`^XD6SmjW-%xxJov&WGtG+ zctd`@zuMWtI;I;BoD_~c&YI?bO}*fca@iwE*(sct!;YVx+{i94!6J9}nLs(ttNssK z=JIc9v`FN%|M*Y7evV%DY|qdo8~tv?CBNPK$z9Y=ac%o$tA@Af=PbglWv0y4H_MOX zaeO_6%}j%1>%r6m0u1*bu~mF1cy%W6?C-;GZ*n*5PSZTkI7=afD_5xGpn1GqS+~jE zAcasHyMHcC?}{&#Jr%IHFl(YKzurd)sm|r|DjsBqE!?y3EYHWf)qCoB)+ydgsOvqu zB=fe8;qK?5of}K|R@)T&ZM!2}YS}NetN0PKvTN*(+I{_3!j3vDjW8{|Hk&6;=XAHR2f*65{e=xd~_>$Qy8QTG0=Jz2+2>6J$>2)?7e?_cKrYfbe# zqlKA7t8+h0DL(nKRC@Av=jRiSbk(1Je9?B#HLkesoT~jR03j(?& z>+QCw72JQdrR}nmX>0M0+inJ~w$YZCRnMlcddC+0M^T?`YsK&1-oK|E_-A>`p_PHAtuP#|1U%{(zm_xvC0dI=e(X9Vx67SxPO?>>ky!>>E<(%x2ga<{; zo?oUHEb-dME4DH;u^_K5G5zn+%NOs;MsetU`@X(DK$#;=P<=+i)T2K0W~W5X&o^5u zWxpgR;F8omo(ujr7CA{@efJ||9katO#gpG^RhUoy*vMtM#qS1peSrw`_6-~j%8R{v zeAG`VZ8WcmU{{vZh~1G89wf*&H;-YX!2MerS06eooT_o5_p&PM(HHNE6n6cxJ!tWr zM@VbeI?KGvd+IxvO|vlDb97peOn+*Tbl+ylZC8za>UUIiFOJ>n{+1s7 zOcCCCX_M17spTF;DW7jeC7cmEn$jbxr+m+(s$pH#+q-j@JFMldKYl28k7-J|_nQUp zfB(KD;@oj^$?HTzzp!`a;dgq4bR|lXp8jt8cvP(OtclcBr!5PPeSb7-?xQ#F`@bnT z&vD$Sb!a~nt1dM4($iUiF|UHH95m;j=t@qn|25TR z{)&bv*LyF&+@_ph8SM7?dfkzF)+5YuO*=IwI}1Pa-5*`wqvNA#PLo(G;@_m4NNCUznFm!nQ|RV&V=hflf~yKa7Z zcm&&u*FV*Zr8=+K$Ln~Q{_Axx`rC13;~urR#8ofND)O(~>@smIzWww2A2#2xIrY=b zPEKQGY0#f7RG5D1pw8-Slh_$KE8Udx(?w;O70M?*HNG5OV;bqUansgm(vB;6tj}%` zshjvbynWfT`uNMMbB{5Wmy2c1dX{oJaq*Ry8|4wA%Qxn-3+bt>JmcIy6Lo@^-)4z_|5d_Q@&2VDzS-QO}do-*o8 z+#S1zedc!W4}RvKD(^%ysIW6kl(=^9IL|jj#uJ7!UBB&E_OMX&-VU==?JZ9_yFwRV zTeL4~;rSjdNlj*lbl$+GYiy#oziryM#&>Vr>=c=I!gGH|AFn^MT{hctcQAKO&*ul* ze$3A`oBrs+Pxq;9Q`cX-c{6(Bt$kak@6(w3cG62uIo@4IcP)SR%~thOPx+UrCR?{m z%RMc%&~a6v(XX9PW?Gj!=xjgjDZXc4{l$ly6YCW`n6|eqDg9n>?u5Va&oGg++RX|Z zCmsrZpIMN<{OrNT?q{0QHHrsBRXk6Hq`h{=NroG$SZfx(EF=Nf`i#O((D9v4GabuS6 z-g~R7r++Dx5=nVeJL^sPOdr0+I6LjlT_;~`&(N52J%VAI_NmvEBInO2?p^cTL#(E_ z+qbMOtsrf(`%Ouc&dEG+YaOGX?2Y+hUeT`QT>qptxIS-J$1jn;J^70F_PU2@x3^7f zIMVrQWy7;A1@;%tynlDaKl8hf=hOZ7*n(a=BuRQCpSDy_&RYG9`|#~3D@CXHjd&WSQ2mOx4r0eYw8T#4}P`ScUF}DV>Om4Lf_@JfKg2=|av6okc4`|A(2MnyzK3 z&d{LnyUC?qB45RJOU&s9Gbew!(HMJ4+w=RJ0|D0xn}4}oSbFMApZ=x;WOKQvbA+M}W_BV)E%`N|aq?nM8Rzv6P!-zd3mXI1|gpVS%L z^()^sw!pOE+g71PC$|NURGvb@(!p1ZtUd8hLuy9^bt<@Kv|x2!)W=`6C^WbgNu zW#3Pm>rbsSKNYiAlaptUx!9D-bt?+$m;L1mZP}E*IFs!?V~n%ns|CBV7j!6ZwARR7 z(YB=dvv$tIGcHF%dscr~wBo3dj)aG4)gq@aFZS$iK2-c`X`5!swj1edLe_LWORJC1+-Vk6 zIy>*_H*K4p?!hg|YcEU5tcc)cIk24n>Rzp~sUG($W}iA~qbxCb|I^98lvdxbUtnvp zd%VPBVW`i9oKnW) zHId5&XRes#sC}&b#roC0K^r}N4&BT;bB3e7k9+N&3&&5dZg_8edH(HUsjIt911udj zyb1Vlw=`Is+c9OEsC%O0M!WwL(o1)i?O7I9$t!LiT=o8jMi${3Yo ztCuZVzi>{g{JM?PUyAfp^cqAsJbH77JNoy5`?F(b%uQ-ItPnn_z467>wP}6lr*~HT zOX1lZx2K--B-e#PDYbVTH+{uVnv`g&X=Zb_Pq1m_cJN@j_Wb3%Cri0EP5YTIlqq@h z^rBU>!=HUvGDGrk-VMp9s)DMh$V)6g%p6YYo>|k|Z;<-v*PlGjIVq-{#u6Lkg(ItO zPI$CjKX1M2wh8wye6_kV<;4x@j?E6cJ=?XM^+Ik6DY9-itLG0nz}Q_-f5Sc|mv7y8z1P7(8hw~;*idc8@a%_7*2+vHp2j|wl zY0osx-*m;_Y4*e;{&J_vTtz>!%ChCtnH+e@+gw5^qL^I!{am8d>qV%y;ViBe9cPyz|;MyWS>Sy%iOz$oGj_7pA7r zbe?H$(1*`qlL{`m2`$=sc8zZFrCV`9Oj<6_#An^OVp!9=W!3Gf+pbo+!IvWU?T+s5 z&I?|2egDD_v%WTYKKn3nrSt6LvzN@j9bF=)yJVeTuywtphq(3o#UHJ9$E#nG-|Buj zd((kEwI|Lk5@k4~r^F<^bjF2$?`}C2{eEemTOVWad;QziiSp0AL>v43q|fH-&RpQ( zToQf1Hd$(w`I-gKnwvdVtw~Fn@nYTaSKGF!EOeeN%rw2hj%ELb@|pzcX|chN*Y4e{ zwDjc16Rr8LC6sro$ky}t-tJ%XF#gmkmxj*YGG}-{c=+!!F|0p(+VuS4oJ(n|(~9Pu z*G!ptScJtU@a~GG8zSbGO*{K$3g5C%>s>Dg-9Ez^woJWb0r!3FZMMxOQ&!Ax_B%G= zmq&lT<~HW38>)S$tbfAm_1XFHzs2q>u{W5FewQ3G)?c4@@AcadWz9)j#p)drT$=A$ z>GjQ?Tyi#w)4_Uu@y4gSCS6h8qx5q0Wf8f#>%>Caetydd)>}o- zNU^E5R4we>p0?rs?Z1CL%tQ{>vP}rmtA2abSNvYmoA^y$(U<1y{}!CIKI_T{wfg-{ z%U$j{@1I#;DD{{7*Ah0S)$DWcO}QTGUvItc?O)gV%lC6A8dh#Nx0zK;f2PUd`L4!2 zNyqF}%3Ssl|-g4(u z%e<}(&W{c`bv(Pv{C@E7-~G5(`O#wcwW8dbwo&qrub+9-;4AK%)J*Hjq=FQc} z58XPY-ak=#)hdPtVvZycZ0 z+AC)Ll8m|i=CS;*2j5QqckDdxX;Xi(IK^%4Y`Nq6yM6^8V)k8BA;Eal-^M~JVIj^XYYpOU*JfpK zi=KM?_2Ps(*V^P3SJ)pFIeB8o%dZ`i4lPRjTg=idwyVBr!{Ts{thyJc{ZwSu`-`mi z^*yL5px!NWxjJuG>P&fyU?=~oBXS1{p8ZQbutzoKbYoNxV}9xsk&|nCy6ZQX-}AVi zo5hg%ctx(svRiJkc1E!U3 zkNU1YI+n-OH*NE_UpVA`RQ1!-vXAepb_=kj zNUXDD{Kcs4_WAYZBs+`K|3jaI)|=GKssG~k;s3+KOU(Ngr$nv^OZiiKa2}7$6Q3@2 zlcw;{O35AfC#V`c5!$#lQtPUkT!j_K&P@z|w!N@g{z7eKvBxt$j`A_XLQ2* zOXU)Yj>(TtJd}1*?>to-F8bomJBwX9|4UQUgBY||FY;io5?v|T@XI^oQ0w#ePfvak zy12feo|`Ep*L149!D+3WuM#_UZ_V z>WYv9PyZo6@sq^g{L!|0d~D~J zeFy$jr*|l<{&YI((p?A71)g(X?RxXPK|tHnaN)DJQR}K0E4Isgvbr?W=hVU|$#2FC zPbS=mcjZwGjrl+8Z#~!avp%ekTiv;mx%a5uuZ=vtxcOI8g)T26Q(T&ZjOFskU&~xN z7O=?A^E$C>clVP63X3~V{#<3Pr={N3cGffZgr=~zc8N51@V`a685=~33ePrWUEcX6 zusl;ZZ^t2taOSQ>n`ZC5%5`>cj9%d4iDfkzTOEG?t1yt`m;1*ezn`(bv^*m)bkmZ1 zd8Xbm>aQv{cd8$Ad*-?FpyK6!iGP~Cv-XYi_&x2YWtUEvLfrDl?58ZYSa^JX->4xc zQXePnJ8AE8_KF{M8K#qBMdm;3be0(7wxQNNV@M)qiJ%d6n0x2J+qO_&VLm=KAv!f2!;RD+I+E?_KJaTF99v zw@d1w`k6irC9S_%O8rL{JenfD$<85oLsfFfe~s0X&rOkgu+q|DU;QTS;P*RbE;Kw) zyk`gh1GzZ|_P=KD?R5@*kZFsK9shAKW7_{po6LV})>wRec8=lvq50x_W+w$} z7jaFTR9XAeXI~|M--K@6v$^NI8HR_`iJ*0Yc^|+UW?2DIuu!48j+FOecGUomG?EIsX zL#`-Q_oV2=qlJoTM_KatZBI#8PZ8>wtmV2utD{G0dmPiR7YfD=%R_VCm>D}>4rU21 z3|TSFPG>{$F?GSsE2h@n`~QDu_4Q-hdQ}3y{QfH{GU*3{phw`QsqC2%uWy_?k}iDP z_j;MpWbf=P0-T;5nho3y+eAA5|Gl#Ke#V3;ljqFbJ6pE8sor^_c>Drr&mBOl0CYGu_Ueagq+3_xHG<6^E~s z-d7P%yH~vP)cRGQWDXapUr+nCnmb~CN?O_H9@Y5t7e6Eqr%nIk&S=ckb!)nz2cx{< z{tH1|ejkro+Igp()cg?t!GDslN!TBs@I+>w#;Ka~gA*63=Vh?fq~7NI_}xWO`lZ+O zMh`{}<>@O{amySEG<~4|Md0|yv)?YtZu7b)zjFS|xQK5@Hy+#gOl1A^Gaii6OyO&$ zzw}@{%(VaJ^mU$$ii~N~FL*Mh1vJ~H8%w1K#1~CCBUpB6a_!~C%%_iJMcPFOcC7Ak zn#X3nsk;2yr?*aL7q<&WH~!NoXP%?!yTwPv&SBZE(y0e_^)Rh4T<6qvZ=s*Ly>7w^ z&FO7kjOk20kEXx(VpL)J*F0U+n^8mW2e*vE-->8G3-xQ9PYMtEJmBCpYIM@SIaTB2 zIycWUSHAqz>rYH}Yc0Q7YWQu!^lWd&2(k9Vv)9~RvTtf$b9mm&&6Q=WypQJZXPkc1 zo6$hNTFq&z!Z(qP34N&%c0GDqoxV>z7~K#ETCvPS@~Zj1cK5E17w)Y_|;S zch?t-U;Yj(@Mw&fKE;PIlIh6P>F<3Q#m%NHRsZkqBfk4g+bW3{D>i4W{=pUbc+&w3 zrhM;3iiYP*iw{p~3JA}iwC#uA$&kAV&d;oCl8!!{ZsW^n!Du_Z)R!@diFNVxo4$J=~A6fvM}}^qYQ+#*7NndHosBGm1>V>(983iShIF-T=l5riJy> z*#j9{xZ6I5-Z$#mutwn#-}GsLjFyre4mTT&4stZz=rr-*&6>Yv@dM`H?Yu4vyr;hi zWb9?)n>oEah|!8EVafE(L5v8J};z9LZ?K#Q1$WUkqbDWAXO77{;5- z>}|*6>*{{{Z;wl0)LT>65bpk?FqS_6Jpr3XF_t+Zk&ZmoPG3-@dGtaStP7 z@ATw)MsG&_>Fetm6&b6iU#w@$tGEBr_U?PMtebY0-5d|j(}g9*v0N!P=Onp~>x_D|*^8Gcdl}@>=o)!@szFI%%#EbOLZT+&|9pr`6tS*0WvruDNdh?Y3EI<^K02t+DIvvLCFNmp=85bhnYa zY~#1vof_g_5}x_)YH0cNr)HjHzsB2g;mGBG^OD|5XT|R~H58o9Vs8E?dn*@@>Jy8E*l}hEdyW*#JtUXs^7;Vzm+O;WO zXyV=je-@>5RX0XjSUXQU|3k{VQbI__zo%n+Q@^X=Ty6dd^-7rwZhmY@;) zJ<_fDb$wA#y%^i%PcA7ed(=G{mTvF;==S}%{MR*iE0??5Eo}b%%M*a>3`m@ zuFAN|KxSRiQsGNV!bftie68E@ZRUlVm@W7J|F}_VQ*rdnfkhwp)Yn8GdafSq!ZZEL z{vVtdZ7zSaIkWJirsz+l6Sn4dS2ka9zPV|K)#JS5(Y7{+8UoW?cW>P+Fn8s2R!PSN zXDh;Y`s%NYJwIu(v{~h6gLgjey?G~U#MaGBTyyBD?14DePd%&iQtGamY^k^C*}Uc0 zo16#sUUy};dOj<4{`ae(*#G$Idfm7=+nJUsiv}8J@jj(Mev_xejXvqj>I(O3(Y;B9=hX_xcSNXUml%R2|LdGYT~qd zzf;Ra&N$3eI%#${YEp){(UJ34eOIj7cwzJ3FN?Xl&b=sf^WU4hFhR*sb5qGGtvgp& zy-_e%ylL|4xBPTtlQZFRujlSvq}_hLOYPOa$^6dl=CT!8FE>25ouIg)0cMOKX=hyCVEdkVxfP} ziN);}=jERK`BGn+UvFXm=3xEb!}lj99g+D_+~>Sd?c0i-w&gmqi&(NlPJ9bHP$PUe zG+#4nL)4GMhgcu|?o^gi3^jXK98#F_<(GJY=$V&R3Fl^|=p1@`;M!A`-*Xl}OpmvV z?|46P^$zoY6%)-5Wa(VWG}H1j$$KKWs_16TJpEUD*vh_jt#{q?w|<|*{AuB}*Oo|f zN-{3#k-c{9=}X;!Uq{qlpOW4r5V)}+?;dmhxz@z}de3G?wKItYioZF3`Et&^cFm=| z(ae@#@*5oURo^Y%!yKMzBEx-6F4akt^JrVPy85inFkLYx-hej(iaEC~%$Z^$*}YpX zBE-d`_rsJscS5xsHHTJ3OSiI)2e)|O@{m)M>O|4I? zlX73?evXUj&R51a3JWTVnI`=>88o#dOsv?1^IeN_;HeoWHlIs7pIClLDnT#ja%LOf zzs+2yCFakVy-9{pxo>Xw|Qn zD~9Vn$o+e4Yp49Z>fP1Tn>YXAa!9(jo9hBgRK4cKpUO%LuSxzXS(`8a?B%4p9~kpj z*Ok9&Ne(G>yjAtocK?GkFX=n_bz9$G+qdzkUevqh-5*uoxFwxF6J_RpC$Q0SSMIEN z+u0l)9iJR4Ejpw7I;D~|YFDu8ttjOSyO@Ks-+k9pjB3Ag$~GYT*A1s>TY3fhPP8RN zSKf{~IqRrzJ+rHa%%|HT*^FQR$NJ^>oXF4@x3xN`9Co>9bCs1)R&Q*M%^ld2sfx)--dI%K_DQmtW3hJH7G83B%SpgV_OPdm=v{^Wb|M zbTMAh@I%qR`TQnFH%z=~-j#RyUA5Y&oM#HpE3L#oiRpG+NZnK4xM$6m`6s}f)8ZTlju+pi#V$N$E6FxR#$`t1^lcLi z753M3E}X|KI&aTU*8aY-+fUC->f6y{b<5HH=b4Y27yoO%Xr3eE9w9rmYS)=>TW_Sh zXDko;Vxh@4Y5w)1ISYG=;}$Ef`95Vr_>Egl%CgtraB19? zd=T*^d#m5(HNSoT1^;-iw=Gg8kY%O{yIiL9=0?Ykw9T`PJ=nx%Sgc>){_lE4oT47m8<6L&g^Eq_F6Pxt*-i^*q%$q2|G-^Ie65%4%DCj zq-GTN#{E#$ch0_AovBw?{AYx{PwbK}dh*mY%tUN&&%f*crMQ>v^KV){>&NTf=J%I3 zn5|x|Nz`DfH6Sn4@!ZRp*~Gt~g%0$C16O@BH!y(Q95D-EL+5E$@=pq+MsYKiZ|{8cEGO z*DZfwTi+Ii)!YAHeUz2@+`ptLaY$P1Tb*T+3IWo2b%W-9%CELY!T z9tt}9IO4?L`XlKnoUG-aynEaxiiL&#-Y3X=qjO>@_f~~P>*VzW<}VF;Yipm2|GyScWlAD{B-tg@0H`hmZ^Yk8v z-FhZ;`_VI__IuA2Ygg?Mp8YG?{p#LQn-vG@U+-|ZlCyFnx68eH6XzLQ%Gzr!Yy*rA zJlf3{CEm#XQ@Ew(#Qs)|{_yBaD@>K{{4-gbrqgFNk;yYzc8Aa2Jx8JoKRfd!9u*C@ z@=bq!llSD!A2*K$yi{xt)*Q&o?zKY$7DW7Pm<;@VoZ4Q!}Ub>@=7cH^VUZ_i_ZDkR@WcQ zxj0d5#m%KtVtVf;N@*K@Y7x)03AomOo>?mV3Ez^VJCig%G$haTdi=XF>e#K0YkeCe zAME(7`E%ycdj|IoNc(XyORq`FQlC_^`=R_|wP(3ilg_)9rO)|Sv+a=M?c+%)zbi_M zUb^l*Ez_>C_qs)&YaC@cP^<)w_Mx*Q5q$pVSC7zj-M7jE;+z z+dn>D#*Kl8Uq4q`VpYh`#{Yjwcp<;F`%k&H{u%q0`Q5thTlHu5^6A`~`(Fl2nTWhC zG}~vr+GNW86_@t$o-tcnx$u=#-x^otja}J>waklM!aJ_4`;oVk*GDWfG+jm0?dPSw z8;J(?l^HGddRkvyu6Rk99kVH6y}gM!Nba3{YW2f?fhpnFq*LQpg-qpm{O3SF_f7qE zVslx$n@sj&&p!l)1Mh@&EDXj&T~9Zt-oa+*VO}`*Z3IS{%5=9Vr!d8-!})2 z=r7*9b$-Td9PN3`;addcR&7_=kn&MCzxrUB3{PPFedixmarOP%o*3CpURd zL+rvYDYm!!J>7wDhP&NTa0i0PlR?_LLIbC^|?%hm6iR8Vs6 z;jHBkUcP=eIcV?N*>hVbG5n0)yKQ;K<0G@r`MSJ6I(PSpQ&V#_-p-qNmEE8>^t`qG z?i*jKxQv4$o-F9A(s+Lf-F~ch zJL&e@4=d|$KhQh9|LvyjIy0hs92R_znqF`rneA0^Q1M=ewffsVR%i*HKC#HTuJTH3 z)b{36`Fs8Iro6K3NxQDRRP}%1)!%bIRtualhg`Yow9yTy)^xF=r@^0!LF`9=eKOqJy5S;P;$TLmX%-33r?@) zfh`&5?9MGY+q*e2fMv(~B)8EjGmtDChH~aODgb9g}(YdNK&t$xGz9^@2s-LmVI6C@;knNf) zv$>YOP)wa%8x=0RiSJE-`)TgzFG;TRm894%pER?MSo!>i)2^igz1*{vOfKz=`*O0P zBd6WNwC1dL%I}{ax2N8Hl3r={x^AXPYvG{{8OOfmbxA~Bv#<**U6LL<<5m6peOGqP zy`fj?F08F@o%?YjkLB#px0=^JikSKSf2}CAU3x}l{9--#S6iy@-(EMp`rFob>6@!J zi4^YTzgE8SsEpOk*JbY7(b|qL71r22-nJ!YtL}EwDef&9B8K4=@@8TA+n-BL7nmfc zvu@FesvS$Kez!+ox&2yp#j2_AieK+qTUpN;dgxsz|E5Sku4waTkH0vcw&qn^%T_Qy z%W`#FWL4Dk;sah!#N8wJ@-7vfV^UkJ`jEM%x#o$46(n721-o3oI!tYsqn(8DC9k ztu1kQy45Cj_2bK7?S;#(OnZNB^R+##J(^_;9tzr)X4hTdIdYV3KMT)^_0yhazP_-% z-cFkD>c;=)7ukGW)65_~|DM>hSC`e^=5MyLIkx-3$=!>u)ldCs_W8t|>D_GmB;&YD z5~jRz>X@gXI zQu4OK#P1iTYEQp*q%~jzOUL(w$xk-lQ{R@5=c}~&5BtUWyRN>Dx^t=Snb@`cyK;lQ zZ5KXl-SqBn*ZW!5dK1^YtY3WXSDDAjLw^q@z7}m4I$FAO+r_ol=Q6ebd^ch3^|{dk z-Akt&m;H2F_JN01UY>D4L*nyyQHm|s4>^^lCfv|#YQ6e8q5YP}_kG89#Y<*<*OtHc z_T+zF+w;YtovU0E{~vs)W8Gi1XW6od3w1khFWbSsrRIFep+wzzK`OzoV_xXk*K^%o zrndIlv$xgH?#`|I&u`SS9eyXE{<6yT<^z|dtoQ1tEf(Iq*i3u54r{(m$ZM~s|2}d% zeY}3{@4F(_+c6@yw_e$p@jd-Vk-+2s-=ecqQ(v9@&%Z}W_rKwMw>4Ym2hCY4yy24R z{;3H(K7Xnj-}^6YiMbQKAY|@~J3FSw$E9tK$$G-sX+8h&4t34@JC<`6o)9f+Iq`K* z;>NRHU!#mBpNg$N_I%xb9$Wr|8O?IH`!0KWM_lP#e8}qq`|0>);a{f4Jrvh+*rZgk zZNr}Y8^MbgdG)BUAC0q;Ik>W*v?=PoX93e@p?d}0|AeXzWvE3q-|n&f5Vqm$iOVOW z|A=ZFExg%Ulbp43)AMwLuf|f0>sq;eXLsN65|5h_dpqbs?dKw)H}!XyKGRZYivFUf zAeh;-=tS4djK^!es>9AO#jh`!aCB1LyV&68viZVGp58O{Wj^tl+x6Cw_{IXJ;0xB# z)zxcncbV;REuMS3?yQ?@{@yPZ3=Ua3 zjeMJiiUY|G2d<0X7Cds0Q*;9VDIbI6O}yzd^OpxXzVl`{_2yp@-Sgd>B;lj_+6>Plk#mm!$f(}@Q_%GGGeOh-Z55kC*V(DucKN*` zHO66^?6YT?jSX{qKcz^DU!J_WJbi2I-mc`*6qBX(S>N8ex$7Nyt9K%^OaAc|<$2rY z?fmO~L#y35c}=4JqKC52Bvae1ods|9>i_F=SmAHnGtu(sg_*l<9@y}E!rHf!=QV8% z+4gs;&c8cMo-by-TH&ALc=W3?x7_thN3v5NJ!tiK)g*B!;o7YO&1*zuJ<4`S*C^gO zd%|>ox&N1@EqsOb9O0dM`XwrMT?;fONwb!vOFXV$yKbuJB+Ko7t2IySygGesi-1td z%+Rg>W|&{@zL5M}?DT>chjzVBn{_SH;WDS27kBcnf4s|DPM9!e)!Q!%nvyv6S9@^N zhglQ(TNGwpJL)!jUhfCPjSHA#4ma2;23@Jzc;)T^J45luPqe_*((jUYq90E}ZpP@aOr7EZT3j z^8McNty6w|w2x$Y>EEh%uN{^#rPq7?-tIR=-n{Kt#N|t(lmDoP#J?c{>WzP@9J zO18w>U)jvjur z`>pCi1L5rxN;!|}IXqgwU~1G4?Z@YM{uid@7inJpwQELOU6Z5kakGTH!oz)EH+FDO zuHUjf{W*uIsoG4|=?ctC^f$7HOq=rlNrcvqna@^L+HZana@n2tPF`lxui(kL2_c4y zJ|3R3PUpePbCEZVR@bWSTo(OFjp@Z%X$A2bzbk4tKWa(+31jl&uG(@@mowb@Nppu% zdY{6cz*lRh1)Dt*w>)8d;rE%8Pc5An9Wo!ED0Y&sS38vxVG#XAmnEZX<)(sdC+#xN z96e-u)S}4vQMN0~a>w7*dMr<4^p<#we($mgV7R3$esMvs#d&wL)n!Y!)_TSp-rkn} zD9x-)S8?sFx9@HV)NQF2%k8>+aK5a>MUkmFTi&$iH9BQ(x~{V7?VT{4lc9^Qf4g}{ z;o}2|^=aWx?ljaphaOgF*)2K$cZ-|M?=>r?PhFwYSTXUM5u@e><>C!ZHKM=N3mSW_ zJ>8`5znbM&wX4LTlkv5?-cQ|D_EPP>w#q5y0+S0Ssw}rS`79HiTIbCBb#%S{^h3=X zo^l?al6!ZCe%h01r=Gv^ST#(9x3nopUsql;W#PClFx-t0Huz^m-U;-4bco$FHnXYZLm zgU`>!Zb??>oY&@y->M#dEO@wVqRyo+3?=NbJ33cuoY&ZRq~7ZAhviLkqI4#1-tSVS z>MFnE-aaKKp~um=`wP<51K)1>Tff1jyyS>dScFy5LcXR53!96z|DOJf*kvdXdr!UN z{=Za-4$}vkTpblt?#s)pli-Mpueki_lhCZEKcwsWVu)d9em(WlW4%drui0;B?qX<4o%es9csNy1CCb`NtwLP4gHfs$^~jPOjwsd1FIS|JSa@{+Fkm!+4@UiTW_uhA#-#PJO>D zrexNFgU9B4v-MBEt@I<@-tOPmpWXZC?y3C!A8Bi_G-LqZ3rR{CK?&_8}sJ&N}I& zA71m+j9Z|vKHfB4XUEZ%qh0{u!HhlUz|4`SpvlVTKkb^6 z>hLRVx95dw;WzbV*Y5Mqt1NaE7IdgiNoCHmIyfQB{oa*8ft*UttCx8+Ue~Ms)lTA# zbgYlCSomR&!^dU*mk%qh?z?~eSIYPO*Vt5?IO_|3l$QN`@8H?(U_0s9)Q73}1ir}I z)_wZ+Q@=n+b?IuSu0Mia)5E{%7k${?wN^7{QOBdLOKwM7yw>l&Q14&w8l9h_^YQ=Z zxiu5NN$S;_$^0{aEOq^*Xk|^Z-{nUw_n)oMEHAoyc&A5XEZ6@}8h3VGTN!D*X0GnB zr?v+*u2mk6UibUQy1?vRw>_8D{o`>;6@A_KUs@(gQ14%SamN3%{Fmp~$oxCOCUW{q z_+8c8Z>RR()GWw(|9{8)CyYTJ_4N~We=S@*TjJuz(*=FI&F`4?O*79|dDv+AHa6tu z&F5lL%j9o#RGjDZ^Lwu86rNMuBDngn&;|KY>!`XN)9XGRo0R?Q`2sWZOH+7Eo*vwC!_;|?>pCOc?JrBqbH9Jo5OMn7l0}{ov%B7JlTLrfHS2^Z$t=)pPco3g}2GHo8Ox~+sKKze>)vq&maNJY~d)>F0HCfM#&H|ThjBRV_#b-m!BmDh9D#;N#Z zWl4UVSH2{n!r;eUu~Gwt)q%;&B@YOteXjm~BX%d_yW7$0#T2?;t~fREhqoZB=zGJW z*@2d?O;S6~I{vU>|KMTQe!p+WRDaRaZV!yHxpluJ z@tK4W*DuxP&ujI4uTKo>j)_~kEr7-O{*92&S(ek!u^Z&=+~e_8)#!YF&V2(3w%9h| zJsXv+Iyd-jc>W;cjO_lG=aR$AGTA(SE_>T_OzAYM*sG6~UQPSci!C-pzGwfDe!TwJ zVk?oGv#eXn?&|E;5mel#Tb*>`O5FRx*uy_PyVc%>o=*DmVap*4v&}x=6u8*?#eP28 zx2mg3f90Ca(hdD_`}@CS*UwC5m@~7q{m+EZNd4CTS8rraz4~<1thCbgn$;3rQ@%)T zzWLx`(Z0VSa_5RqmL1raHOtlZ*6qU+zh1q-R$0H3{q5paf$?7$qfgJP;Q!Gq#b>@* z;>NS(+kVek=&aG=5c*MV{r`;11##1sf94Tz?0k9QYP9uI_KuyCRy+TFt=GHz&E=|g z-?C}*x%&mHb<^Hm6rR}sc!Oj0^-Ae=*KXal`8VNN@s&NVUMAHWCH$Vf>=oS7r&AesY-Rsa%+D>=z3btE2Xj9<+nt{GI#$EaUi@r&a{rI@sm%2~eBkzZCMJUN-SB$%t3ugzHaaQciri$C78`SXhVa$x(!tI{>W zr?0WQaytJ_xP5qa=go6xdW9U+=cF%AHRbP2KC!%dO3?dVD!A7A@l5|@DI0ZdEdA*vmJ{{m%24^Zc)Ma zOmCZg!e=t;FMT*uDw@bu=o@NsxYqRjo_EZe+UcsF+mDq5b2UAku2nGa{8kTjnLDj( z&iMx?Zb}iD74_Z4EwX2g#`Y-7liFKM=k(Y#G`%}Lx9|&(!`8i*WUl2}a29NN*4S77 zp7H+{hU?N3#15G)%1*x0%2f|r+ea@x6MwF(&mrk_`9xGz3BQ|TV4a( zwzzI%5j}r7n+yGmepFx8d(pW{vwu^}`!ZeSCELukmVZ#VbhqKlEA80L2jng#PI=NX z_sUv<^E-M^^QNagYs$HmH#gZ#ZU0ljEJl?m-}+CFoa3Iq*#Drd?n!Zr()}nmmzCdy zXK7yg-DkFAOM8OKIwrlSGgsKJOgxf3uf9qp{?UXrwW|!eE(abuv?`oaK3MtenX=Gp ze%jHAXA}QrBy;cVd4DmWy7q$imBaTv3oK8&bAKsV^Rz(R;o4c_XCLDWr$sReFY@pR zv}>>3mYC&If5uLDU-wpJp~Ifdasp?w{yMz={&P`(m}}+EHFpEwN<4_xD+s=_-X(C; zpCgVZ@3uZI{kW&DvMlXXnIzMuvf~-InMH%evZXs3vvb{E2>3U4wEeBQ)iiUKz}fWI zS>>~*-o0~ea&g!~h1hew+8>y@YNfu!Px!X1yr5dz{M4G9SO4nSQ&^V0yp#L$sgib` z(4+1Cx0aXJKloZEJrNqu8lC7G+cA>;iPUam#2zpL$H znVBvh)O@xmDeu?eTB+hMQBohKIdT@n*|)R0KGd~eEObna&Gy%!%081lB2`vg7EbE* z6a8nWPL*Z#_`SI5$+Jxo^^X{Si0FE3&y$$1GdEL5(~v*r^vzq+5KkNImf zTz7hYV%6JsxBDh?<~~)nof5ucN9~dC3jbP6RaMw~d<_<9ExCDib&K%hMHl$$*X;{4 z-lt*T^{@8ZJnKc@w65LAwO*>xtHIsreKBHv@2PFs?}F#6?3%Lvf^F=Zy^-&%Y;R4u z!NwSKJ}~ZZd2W7U+`nCx&Fgk%clWN7%B#A^a3pSWpGUFNy3Pkmzc$BPU9KrFXrxRIdX^%(K#7E#v-qcJ-exe>TsL;_98R zB@)B^!+%fF&eihX4L;N3HQo8$4){47H;6Nw?PNQnuRON?O<|IXfNHa!qv*H zooystnHP3XD0;vBUX9bYOr;OaS)Mt$>}KPs8sj5-=hiP#pX_G-@@~l5x{5peWtR_z zya>Lj6|$RAGm*7$7pJw*rOg)?e0=e_U9|B0rOe<--#zB@E>bo%cUktV+Tb+9{zS=F zZ){fQZ&~nq_F?^7_gVzy&rhG;k-PrarQUTf-M7~-Re5`0!Dh3rrH8K62_1X)zgh2C z+zS)wyB~7ieB|4Gi>-dut;-j--%oOW+gRUfyJnBqNsrF8^_k0#iaKOldmp`$`)c*~ z*c*C3qO)ea+j+lgy#wF&^X8J-H!9qoTu(AvRrZ4|cXev?E`!?Odz&KFyV#E|SRS~v zr1sHr?TLrB$i02@_wB4@QmcIT{9Ge;RgCk>Uj2!@d)dA{Jh8l}XD);B>fHLrabNkw zrdzWm@ij;-xe#?(P2%*Ixa4~ie}qNKO`ZI#qNILDP^wX^nu4nA!zIl-IB(DDjg_+& z*G||jKk-KLn{7(6vsnZT6ke5lQ82Uob@A_x<_`|_9jQf&KZ&R)J~!;Q;!)BYa_tO<7FmHDi%{0noZ<5`` zcx&d`88!1=t$!WBQyul3*F(d>Eok^UlZnuK0kZ3V&iVcx?9UV^O`$9nVQVCCt7dC!biX`#9f5 z^4Lv=W1n>9#U5!A|7pi#W}ZKm=Sn*xkEWmGjobO+(X{-N7oSAT{dDr9 z);_iy-LeKPvH_Pat#&taJG5{3re1QmIpfl4cg~)5H9PEWtc0I%_?92=+2_;!xuDPP@q&3$k!{bo z-X!RBx&5-z?n?<=ydX?Vdr`bT+0AV_O!Umy*5H5;qpE z5uB&6^;qXUk+~l1+veLuMTkaphVH1>D@kwFzaclRsdMw!EDOHx2`_FioK>#iJ!n>O zIQ5?HlV@DlcIt~PsVsS$e)0S~*+=zG4VRZJ-hOyRojp>!tt50Sp9X=X5$w!8Hf8l7S37N zr%``uLgUGm6Tk3PrT-MQ`_U1_8*|6NYmt@y<|Iv4XNUPAPu-Uo{1l(5bwF^rQ_$qM z>Oad<&;dH$6)`z6K&O#?e#Kd0+o#8pL*VwmP zzOyRZKBn;3;X_`RFP(hwc4}szf1IHN%dv#jv5OUz zcKtr)yxX%i`b=)^_Pe*(8tYg8>u2xU`TNVaP2cA3@PDVacl-JxW2W|c#YJC^AOEmn zbfC49DK z5^uwdIcfE$rFIlCHecEQ;JI|&`7;xIk`tTtKDcm&S6*)U;nmDzRBhX15_-mECn`ucIn^fbX|0j_3Qa$ff(^75M- z9NbSF>|!WdUN^w{NZYPJy&DXbmKl&}v z%j(kEcFk}6KkpYm`1qc`{bTrt{rL~;f3qKc`0;^JY{cZ}<@!H0rkGvso*xh-&h}7s zLVbtMgLkaEC9AyqnT~c%s{SS48e4zEP(z|iIp1;m{>$9weF}HvnsFbS&UyW%^zW#Z z?%BpSYadPi*SgXA`lRz#=XdiuGmB}+dA(j-ctp zt8!-z!u~z^DJ0-_lR2MIs)AHG(_w|mRM_Rw%r-r@$bU(bI=g`WX{U1#4 ze`vb%NX0F#mv>h^`;%zp-AgWX@u_jzXU*^Tv0AP6>&YE1tz;vPi*Iu`&N-iWFoJ1i zzjb=g#co$cvy)P9g^oyWOXJ+e7_yh$SJw7S?wzZ!v z?*fU|mbKfrJ5O0LLE>cP(y#lS><&$TBki7hKfT22S5C!;v*!NakJroQ?Na#v?{WVA zdV_c6%g%dVynFKRyMLPxzn|}*_wkqCGs(a5Da(Ru7F=4?n7?0=uXo-asn0Q+-#$;L5@-pe<3^J`@PvU6@f z_1-jMpY2&b^C{Ekm#ZXlm(}iAl{FqxH`IW!@@Wu4am)ZH>)!S}kt`0ia+?>yMUhc)5&swLs&PmOy zwYL7_mL8j4T;sfa&t~@J{1x1<6Bf76nD?kY&1%axhU}WJS6{viw`zC$(BM8nZr;mK zy?X6ENtUnQ==}ZlVD&~Lp9hgz$%_yEJGkR;2BXpYdgFISTupZlvnTIo5d8^f1 z+1gWQ%9d+gzW(yMuFfK!)og7_5z*1kD`F%Tg{JQMo2mAz%eRNu^TydSH|=Fix0z3C zWu=;2+w9u1TUb}lX4{jM>>00$H@=tE6E(l1RsL~Py{u5?vz3A&pZWW4zun!rY^zGu zp+^UkmmY2kN%5aSqj*W)~Mih;(Fvr4?qe62GszW%Y%1vGnwRhMwJunh%fX$toM& zP-u;eFRkY6UA~DWba~FPn5E~O)_4_~Kik2uU?bbcl!=w@dk<|3TgJ+!CXrS3Z2R_? z%FjfeUJ+_wJnn3`tf}+IwHw7(4UMaoI{#)1ejt1|XZ_#ysOc8e4fKYLc| ztL-OFS&jUxS4Sh?FgLHi@;dwDbf#xNuKn15zeU93)VwWgH@*5KWv26Hll5T@sf*5Q zEM*;fOFutRntz@}So!iy@8xs)xK42Iu3mpu;BiCweay6jTClzWyVsPkp0yF!3) z*371mq|Z-Zhv=!EO9RWi&8Od*Fe~o&v25oH!H0F{DgS%( zkMZC09lK4kI+D_V|A;qn3+&r+O?9uQ>Mce$@70Bqg6GP`eR_QP6W2}?pAfYpN?(;) zl?|l689e!`cvDyU*vjVS^-BHUSr!KSzu(Bmu2FN2)6ajepmf-lFrj_tzOj9O9Z`RC z&;9kjlW)Is>e?o~T}o!|;8(bI!4JRJQzq*i5%!AK6eei^_@T7uV?Go z9d}Yc+KONQRx``4%BSkov;(tmFMMz&Amo-;nfMdc)-m%HPl-P#Mfca$mou}do=wXynrxiC z@nZigFsOQCCvoNV`TpRW%7HPDo-M1ln_v9u;l3+YMF&(q&3xZsQ8cYYaQYUHj}<)} zdVWe)-N&|Q8+_2bSvv8Yfba87?;3hCQsw%8SiF13e&PG?{`5$dj-OUt_4WIA{jV;` zX))@0>c6phi8$vR_rLY`%iB#D&Q031T{g`9F^_N6_Ls8N%$H6qzi{7UEr+ISo!1SMC=oVmlQWx1v8zy;~os-A0XFS2gb4eogFu|oQXhPvby z0gaqD8A2N-iAzMRe3Ejn%9n{H>8V~B|FVek1KQUwG@h9*{pRq~dtyH~uH7(Ui`Pag%nq?bXCUmMa?ELj3C-r@b z(iypi0E74MeJA{!T%{m<@JR2yH|tb&dmcBwky)ovysf7Bw5ftcRlk?M)~xyep6~zf zE@Q~J;jS-ddLWzm?8=NKxq25JQ|?`hILNU6?BiXT+L1xde|N02s%o{%Qk9v|aQBw< zy84Lve3=U_4OMCECi_fJ-A_s7>@ZxvyW+!)&=03xPh-7YTX%VR=%U$v%THGK8TS>P z$eS$O6Rr^^6~5)F&;^4Y%SE+wLSCw?z233CdztL9AV$R{o6Gd(9}fvXX1844v!ti4 z&2o3uF2l!(myOrtXh%GsTv5~Wt}Va0Y4Q$9w*9IHuhp+Vb#i~o^*n1m)962{>(A9S zc;6D4aB!=BuaHkc&Y7^obJS~M%8C}Q`QhGTmnl)c$JnFk?rQ!t|C~p6uf!fUpLt{# zqqFG(7Vqp`#uJNgc8INJ7H*E68z${*29k-m@b&PFeJ=;X1m1|>9?yj{tKPkyZLiD`R+#J5E_KTVB1|E6*J+aW_lp(k5 zhoqwtFIeyEyxwnw3T$}Q)-g#a!*){rQlm+CcSkMz z<^F!cBTwC~yH|XwE(l!ot4;bEIeGl;&Id>EZ6Ff+kYM+C3#Z z*w>aBN+kq%1iHGneaPZ%JQuLK?Us9XOThHQk*}_a@|{}v>f+7A4m^P?A4^`{zoj)$ zY5v1IfxrIvgs>emO4d8|--CTc!;1RYoXcqnALZml8~!j|y(}g6U-}N$7WWUnae}5H z(YdR{64RTd!>v!7XCE;8`I@aHA?N~^#zCev?i-hGk2DN-3x2Qvf!B}wN}Gx3dE;H3 zU9)WW1sqpNjI8~0Lf$Oy*AF(`=$VZPC5l#Gbk|46Fx)OJn;QN|NuuG#>n~=jT6_Q1 zhp4q`=udw8V^`kwX`6TNc7K<;wDR22WT6KVc?zo&{VM)x{FOrM_$bUh<*fjOpte z>)x?j*`DO>=DR3!N@h2s*W$DZJw0oecON>F`nsaNs?;Ux?%f}sS4Xz3n%Bk>$5z8s zdBW;Q{F+myzTvXI@66c_Wv?%|xo1yA+NQr&o~xx6C%WHX(qFVPp={Txjk-@)@yy6w zr^B_k@uo~)u-wsOaoxg_rXGq8(mUVrvV??X@3nR6?Vc?BK-pf<(SG&o+4k*L-@ivG zw|t+?ctbkBp`Jk?P3eyFY}e%E{V!gJfB!1JUL(Wt_qG{crps?S*s;$@tu4(ns}KD? zm$#_4Jl{s(dhu@cIyq4(H-2MAo8Pz23fmk%@=_D<2lzaaH$8s9bL!i`R>(hYuX+E-^d2`N)f@ zIUl}T8+{PBZ|;1SvR`EG63cn}3pc%)Z!Dw|-K6{VmV2Q4^~qI}cFj8G-4f_(!SU9^ z?eGfKv&&{E?{(dAKS9kT*0#33`1z;XGqRRnRBPi;I9cpAvxEPMSIp_}M}KMDy|_#? zs$b*O(Tb^hpUht-?TAum3Mrl$lCvcws%=iXycM4XT^6zHP~ry>74n9C>kfkwYPeUTN0G!}0C=?S6c{9sj+4@AvHg*FV;; z-y1(!`HWeaO!@oc#|_w4)qLLQS&(sM{(Ono(+__;;2>xHCvo}Y=qQ=i@8+5YLMbVC z*D<(Af1AK7;QLwh_`21}{DD%nzt_I`cspPD!5M={9>zJ-Cpy?1wY~V7?M!Y+?{ous zzFP6aEk&(N@-9469tfV<;q_zUgA2b)RDQfWno`dseOTH0!DJ=7*n6@4MK$GXxKf-Y zrk{`~lb9{sEzBnNbM~u)Q9gZJ%yrei=v{k$J^kR$mluw%xezO{A;$06hP>Jdzc^2~ zy%$ov-B594m)TWKfoq>;*#;E^rYw)&ze;L_rDW(a#?H>I4b}f0Hr2APR5fgpSkyP+ zysZ6?g@I>Z)ceajmF>)beKhpV!wIgQ@1m`AY4BJbS3N`CRC0yh8zk2sQ`?yrSRYTAZ$sO)%*LoMd z5OR8dzutA*CE-5{ubojpQuD^#@oZwqhFs>8o6cA~a=CacVbY=6q-k@PO@1wxm{w_C z(B@N<(zW{Z>C<-oJ%+30{%mD=w_iWqWLw4MTeCt}1ZQ|RJpS0Y-SEd+{Y0Ct_iMMZ zPe1kViB4>o>dgt^Kb>sj?PcnDoGO)H32QQUGMwM_F0g*Dmi>8y?emN8=5U;{G-WPQ zv&{PU^WXN1TPk0*d|G43{b<5-r=>=Hp38;I?--kMWT?#calYvIR5X3&qUSFsIWIaT zpJr;TA>ZnJ`lrLppgd3Y_b)V#I=`tcylJ^+l4h9QBZ<(+Nssh3W>>0he(~|-fqI4o z%9;i*JU=vF$YA>|F=MCM>bG4&llShPx@+;Hd$v3pVJ{kzBsUg(oS^^Whj(0D!aO;@ z=5K))8W{BN-e6$fQ0JoR=)_UsShrm^u0FGA>Jx?NVuuYCckhOLWL|on_xj=G>Y>?@ zxocdv72ZwpyX@e8!|&n@u_h<7!#P_!Pfy9c z%Humf5SWs z8^7GB{PUy!-{SWBzQQM~+q6yE8*;x$t&KRHxRB{eW6P&MTJE1OCRNT0JN#q$!=IKP z`tr*6UcGzk-c_N?-jXkQ-^nfh5^3hem>}q*AG+_0w>pQq{na3kXY!4clRx;)Tw|tx ze_6iujNc0Sfwy_LOep>SQ*H0P6J7a-+YDDPD!n&BlJAf8>A3o!ma|cdXG$A?XjxEa zYQI#p#K`8q@}{zlFOJO6R}pqQqPq0pgt>)NRTs@$v!_+cRoe1d_KG7HI2{GbOEcfS z67?|6c(HmuL$c3p2Sx8qPZv4leu`eOSg6$SOV5wZt2*Wt7k+yBQ&i|IgIM#Ov)!d< zzdf8<&6mw$B%t;Dv3j#bVtwR4Tv;=v=G>;E2mi2Nc`JP6rRdT0(EOglqIq$N@$8=y<_y@AxgoMRRQC8kpSREBL%L?boLr0&X)*@-o@q8R(p8V(0px|GFmXkbY_3nYYC$ zdGnNuAIqFP{%=K^MX=SwhOhT`n+rN>3(inB@t(>No#`@BUxDYrp)}s}B7I$7D2YpJg9V zY>K(sb|v+K<;jk=gZtEUB$;+JmGV7rJiKIerVz`oO~#G&@)FhMd%6o-k8p0Bf7sLH z%va7Q$Ez;22V53x6pH@8-qlZc(vK%h_cQtV**RrO3WRU^o>)5l?yh@t4cboey?Xc{ zTIwI$BjqObubojR9%r9nZMb$fr9w#H{dNyQAtSBG@2!{j^2KcWbg68{v7}XvdrEot zsND356uGoIQ6y;P8mW5jv$sFHp6iduND$3>(i+pCJ+V)2fs@=0jSkMH2y7+nVFOo(qW%x%|Fk%IIfTPn%Tj*nX$Xhihi?+X1OZi+5Jbl zRIA+c@LX$|H3v_7Rw`(htywtPqqnhBli7Ql;{0dx|A4Au1-SH;w{A`e?uSD0M7bj@$uex+XF7>6m}UVg~1 zeUtLF@!8~>kd0we6&2@dsm+?<&m6u_V&(rG7M6DBgG{y^yuQj}`{enFa%~^a@$F2K zGICg;+L68U49Dc|8m6g|w({M_+ubf+EO;0GdXwE1kIxL7&d*rMEX+P-?xN`8*4T+z zE9;f+o))-#>A=P5213QXhb@nlm+*Bil%MG=^QeNMBs>$tI(uRkaptCO^ zx-L#op4_YJ;xVPo@%sM9VecEHb>g4J_p!g)Hu?O-m)~zpnz!=UoBBiQTKhNOZIJGj z)OlB}aOg!{N}0AxP<8D}&rbz~SKs!n`P2Mqx=E&aO&^=zHD`;i9lPrp|Li&WJpa<_ zsrlOayS7>_FlySqy}Pld>Ndk4g{m8(H+FxVlHa+hRKDdie|O10aTaBROyvcAZ|xm3 zCamt_$d(Fnx8$9ev7&YV|5@MaGq!Kk&yO~HbLZJSC!ZBFLruSn_x0>*?9H8*BWs_z z>c-tYGHZ90W^a49eVXFd%mqKV-`uQ zd;Tn84w9UkXn5;Rg@fFJuyZpMW#2psVf6eEo$)a<)17Clp~5+zw>3}aEb$f2n=$)p z@0>(t7K;n{Y>ri;O$s}9G4*yg`7l*Y*}i+phMiH3uY@-nN;5?qi&A^L;*a7bzs{K} zZheahF+8_y%_i3P_nTO^*`3$?&i}`G|0V5TQ{(C%>R+k}Y*PI5m{&aOADg(&1Rl%I z{tU%st6W)}Z@>2c_`bn%$)RsEPCs`%^R)QL9hn6`d?c6V^*OC8D%PpoU1Gk&yZrF( zE5W<3{Wgu-e#R>J?)M1IoW-YgGbIlf{?OrHDr|D%f5eIektgq5KJUs*j_3&}IN;bl z?PaJz_@xD|(e*n|n9D@vZI`lQ}kx~&yy#04=&d`6r6F<)oQoTdhau{J{H^E ze<8QsPi$@0)Xyb>+g7MoI?vY2oL{G)l^;0w+=J=7f!gwR$D&?7I{)MK)lgTp)Pot5 z!?rIvSY6eg%iNyvVcCyMi>eMRKXc;Vn$1rGHatJb_iTRR?W;9&x$FIR|2$~3-cGMk zy=3X%xq)VK9{jT3lK(9ee*HT6jwyHC3isoBQt`>3GPZn__D+vVGJV0bX8(@ZLy-a3 zm)_L5k~m%Qxd!*mDK@XH?gpA^8$Y#9JM;Qx-nYFoB{Nz6E}R%#7&vog<^_#8R#ED^ zo8L)QcQ1LKd~Bcf-cM@#{_pSBE=9$(=eJ9op7uSI{b{^Evb=(|cwV~1^MJ`V+9BIkTG%f?^ELFE z@}|S=wr4V@&)D^SPTSAT2`e20WHl^BRhMcADIRmp|DvgA?q~Ax-?H@!UdR_;OPzAp zs{Y#cFQ@*gHM>JL-`{<`;fk)ncWH%Hf+6+2hFpz{ z60hIdagSS&MWf7dOHpWHT93-N(p15hN5sMxdmKOMmb}g`((ccfk7r*O&N=To<$nLo zP*L@1>Wg02WYjO&Ah#j=;v3fw7w>fGet7Wk#D@D~^Np^aOsSS3db&cYK-2g5cd3%|8b#-Cfo1%eiEmquug9F?xwJF8syYI)lDThve4{&a|AhF-!(elt4{hR;RWQ2Pke_Cg{ zMXUdb&gV*Z-|&)pj#Q(^301A@Z0pXNgiJm*zxVRJnk7bNXXgF@lUID{)pn8|?ZumUv1rfP!{Aa$Z6qT#mrk&q@!e(Pe*CF9m@7CHb*nR%_s_Oa{tuqUl?;f6a zHuuF{!6^y~+9f=nt%db8?uR~=nfRf3ao@xu*7~QtcMT(bUgi{R-D>^QxO30?_|LYc z!DaUrRq$nXmuoG~OTD^w%`UILwJW5BC&%VD9g}<|8-B%sbzMpNTY2l-9tW4rn8on; z|GMNrcmIo5n71A*z4`awACmuFo*nf@uWMb47# zS%=o-Sq34Jd37l~X;;}YtwX+Fox`+5^@^quK&c(of_7T`=-orwh^$;DBS(2ukn7v^cN2=Yv=!X!*=-bE%j#t z_dYzUocHo`j8W0iNP|;vxVL?Ed*=J@u=Q@S&xNZZ_IOCGzZcl}-oEp1*n{&ouH?UMZ=A4s@ow4L#%oPi zY&e&tRa{x~TdeV!ZQg#@`ops{^)DX0IRE1}=U);sWmyl5j``U5oXYs*JE7}#@3B=| zEpBI@o^!co^)d$b`khUOMB>gAF}$cXb5sqme5dhh|ErH5Z`THR*0I}sulpJ6U@>93 z(B#ESy4f~<$}P0qrfSc3Jbi`Xck>U$PcBL**&YzPer;c==gcIVH<5Ql^V`HeU!Ax( zCv;8FKikc>?!PzL@J{c}wYxVD{88O3^($V*SmEqKUbfW}Tz^Ok$HwZ`vrBJYC2g`% z;KBTPY$CCHmnBY1D%M%+Wg@Bhn-+uxO6t^LO=yYzzD zo#IdDSMjs0e^qhE+h_}qnV9{Q6AXRzf4q;eZ@yX(lg`+^Q0YY__ofNnTPHB}xBq>n zRg-%lT~2T}&;Kz_Bx#9`0+<+O~L~yu_exO%a$!tw(O7E^ClvzX!-3}o^?r@%lC3+uSm3& zT4gf--5%B5h2nn;3QXkKf9?q{zP@f|+{)#F9%uzlb!dVT7}HjY)CYk#Y3|B+!|R-K#Uc`Q>! zZcn!K$MWTZwpG{74z6F+Xmjt+sZT4UpMTn$I-hwq&&j9Ca=9Yw3k^?{xEmHH-aWE% z)3K}1l#_nz@k~Clt==~#H)i#(Hn(#t&zGc2`I^L8CN4hl#@D3HWd5673p*@U`J8o> zXK0zea%1Bw>vfZjZ#7)#*}u%~>xN+E*RLYG#rZC7;Y~72E{Z;q(>vwEtsDm~(^b|N zvfpiW6#9Ba?#|Z@e8#-bRCpz}$?P#xkLQ#qzH{kichhR!s_E&KQycHpTOR!umkmHArbte=l_t#|KgYYcQ< zH2EQ?THL#J4cks6M*b9Ec~`7J%DCQU%DYSTJyweI*Zq$ET@pDjefx_Ov+a~uFxvmk znH4^z>u5pw?W->zFFh_Tw)Xd%X-^kk^NbIku6(RFF(bz@!C?9}Gldrx&Zvltk3UiIbJ zURZChslMyTgThAM)z2<&ieFx*y-cAj%KL~*hvduTo6KH+=IEZ5s(SqP(+jsFn`A?7 zoLTl#?y0rbx~e(TQ#AIpEJzXg;FaNZ?wQXu|5TIHHa#6JzMakL=KaAk{Z5u#GqW%9 z94TDm%xtqH^J2`jmnQo)+WVbZe(XNzP$)9x@W10f>JNl@NpDW~yZ_YIpZ$5$cZsd9 zB8=*Sb51^YUea^tc1SpCp-~8u*qTba=-T%A7_@`>aBRmezzm*!BN4wo^7k;4j9+ZPv2URky7Gy z;q$iseS%RBa%hlD@mHLE^l|9Dr8=8-`PIn>8-OjB@daH ze$!v;)xVEx`oRl~67_YrUjBbr*!2Hxm3LO{m5iVHr6Sy^seDVOtbOM&VNLg)1zR_} zO$e;HvTQ}sj0Y>;c(AAoN=drp$O>4_3w-fzuFIa)lRdg#crMB3`4GAy#=>jm!+Uv} zqEq;H?{U6fz3O%4f?9_udY`u(|1HMLptmVaz2tggtmV?%!K=T&3twvAUGH?eS}x6W z>z`*+EoY0`F1)}f{HD_T{Fkewc_@TU>Tl(S0XONbmlcr z3Yf$9=J8^!&B~=a_#5{uxzBRlM1*+(FHF_a;#yeq|)7gqh+$*Om>X{*}twS4_|O7`V^b~Fo$?^(>H%lI_1EqaTzx}kK+i8cFmW+ZRy zP5*iKH!&pLpGPk@DM%4rME3%|4rUrc0Q8*(CF5qwj=GPB#~1M8B1ey;^az zyj}0uTg4NN>Sqd>>X)teJ334C%d5bmvea1;8yL^63jZRR%c*&!X#&%pRg+8XPNwN^ z_E*^Af5APjTv|G;W0QGNfYJ0*T+wS*$wz3sWRVT|p#DwabI?O2Y2~iga!$n;&F9U8 z+@APcw#_d$NO+xjDf?dkeo-^gqO(iS%5d71lnYF|vFzbj)$M`eu{-LOc!mDd=VvSm zDbH(saE{Tu_5G^fpYmTUF4)_o_0%nb^$y4C-n^En-{jZ4`pf?E_p7{q)6^X53 zifif;&^sAP0DCE=S6rasv1YNMi)bcKUQ z@nWay!Wq9GF4YkJ-97QyVh1x(zIwrPLK`gCzvcIDP}{cH@~oHshc8~cEu$=U%$GC_ zjCy1K&S^(o?5w!>bN|jtUMsqjW?h$_9J&7biQ7)9)i>L|Pjp%R_*;6FQl)j>l`S)5 zJd!R~y*m|je(|blPY++x;61hD)#hVQ7i&#q%dA-=kv@6PTGnlSxwG%N?~X0eNeWiu ztQXk(owL3#)z|%O^@T6ZrmL@5COm!YCt7eoX!kagZJU-YU68oD^p>|{qL!cHYXPsA z78QTC7o28hY{qw*WvajTPJ5VTd27MV-BPzX#CI-H$xxHL=CopoVP#|it5L$QN!1fx zntShSSNhb&vD{H>yPx{|)Fe}nOVUfLMOo*p;PI-TZku3bd+z&$OA}ekcU@kic(rln z--WN3zNLwFaHsu?n0na2I;!0AtWcV5TD{WRcSh!$a}LFvYgn*3$nl~MbH*-xt$LY# zDN=f-PkQ8TEIdD@dBL8XLa&l*%T{_%J-Yeq>&eez)?@~YyT5b$v*?4}vN!S;0!M82 z)xEv2(Cen`k+yoKEAMvquq|flGKg3d?K)NL=c5zP?k=fJ*>yHYCMqfFYqb5Ag`6MV z#cI3m9<#MzF`liITGP?g-ZLIa)9aeVDqCRIfo&NGmLSVwoR`r*ED%dVHMV6Hn&O0o& z-|RG(*M9cbZ?3XQ*4Dk6D#iJ`KI9!!T`7}Ozynvu2`k%J%(%`oy!N=U_(9EvwmF_6 zb7Ze=crI#sMk4$5PB&AD15s()j`L~=o_t;NLwV6+8M`a%op$swF8-zGJ3;QzWg)w- zZp|4A_ga=6NC}wnit+UBh)iS8k28O^Mc#9-{(Iurs(BO5TB`ITTC*d~wsD;BU+-7{ zr^G+x^LLrwB{Nft4Sy`rNHdp?`CfFnl6zII?V_ye&5X6r+pdOLraIloo6e`uu&S&w z^^fHL>rbW{Zttqusi7C{_Q~@`b?w=m(iOYDo_M96d&Pg1(b0+Dg``sKH{M;yX4H9z zbEV0B$9Job85l)!O|YAE=Cfeiug-lp8$MotvADj?WzXc9EL!)sIu#sH_Y%prsbF2b zG<;RmS?||vVK3O$J5AvK)!%ErDIl16`;A-2xf7HRXL8T47uJ3z9KG~{de!4!pIF`7 z^~D8G*S^}F{O0O|h1U(bCMQJf)&C~dR`OZwwcF)CPg>loZ?uG0&ActP(@mNG@WK-x z;zVlWR}{?KU4Ql1qoSRRbwXR6ujtQO#b>U&V$F@Dz_Y7v=W?%By?AKdqTXY@iDqA> zIW0vz`v_($uT3$t9OroLmj*}P$wbIPLj7`EwM zSG^dE8~Sr!mueSxrp@c#Y;=}o!PHsvth685o>QqVSt57);q<@P>eoGAR2T6gqcK}^ zeqQT&`OB4G*!cRS7YHBQt*fMZf{*LHK)j<*re%$GOznX*^P^(DmC-es2mYUv2)=He z&OKQ+Wl3#~QrXkam~UFrAKw19{Nh%zWk*ivoJq3M>1XE5Nu7MYW7lD4nLo<4H3wVy zP8;pF?m6{p^HI4;=683`e0H=x^owxGwW4X_;k}cu%)foU@VCz8eXpLnMY`~=7XKN0 zd)JK`iE#dpai_3E#Gl|YwTDmV= zrfSx1pVk)~Uh|hclSw!Iw63D4V4?K!f~vmbWyWjwKXelh-@zNTW%8pA*16b8f|=t}wOZrGoG`TCB9brm_ky;KzntV$nVm=ozDA6jC% zZl$=~?ETV@rDPAzkd~k5v7W_9xl1bC|}+pJj2AKdgX{r)per2fA6%EJ@W{{=lYwryA_dt%bz2>ErM zYxowZm~Cb1+*hiyQ8`3?ajbG=cguLw#Kf)WH{c%@9 zO2a?R*}4}xmW#Yzn6}BNj(2q_hsXbK;w>eI4E5}#7>XA)eo*JJvB{g&=N+AS`<6%6 zqtq?3Y;Ptyw}n3XX69p>)KtDPT5s->WzR}4n{1y{|Ea4|D4ap2FzQOv0{=N7&3aE1 zne1}e&fPH8{%ZdC?9{1wOml6n=W)!rbxeCY$8E+`$NtYNnYpIR`&_Lykg5uDKfiJ> zYk+!KIP;rr@pDs){iO<3ZuNy`aH@UVT=3W1pz8JCYwr(yx)b2$*s0aX93#H|!t`~w z871nEUX8VJvT)$cU9siN>RJ{Fj;YVyB`o;TvszSlQciYE;I-^{%gj5u%xBG3$S@mk zFi@SkD?m)wsrcaX@Ji0(`y1yh`)Lq+C{*q+bKwl7x%E3{FyEe?Tt1~yqKsQj=IADl z<5&4?*naV!lGjh$oPF~`WHY;*%-IDCFLrXCXaDi`P7XnwEd=zYuk#jX6@Zs&2mms z0atr1*PVHM=5ficTwaBT->yx|+tZQx_<-5;bzFL}a*w^UEKUZRcf5)c$+fecDfhx* z`F>uBuvaZ|*XCTP*D7nsJ-Y4PwRK-&ow61$W8D$ftZ+s#PBJRu0Yi%Vjahy+Y$C6= z=FY#y_+aO{Eyq+gH0*3VB_`pba+BxBv&H@!rqAZdmoYP!m-XL}{BiNw9czy`2?l-c zV?FHg_b8+Mg(p|U#g=DI{r2GWC#$3;wV!7?eoIa`e{5rg)sLoh#s@3wuSQQ;@M3z- ztYxyCZ`r?!Bp*DLzimc(gVwQdL7(1q-rZ-Pzmba9yZAxX^v<&@9I-XIMN172F`HSN z#=N~z>U?OHOQW9I?%C~rcVwF-9(PYt-tD(4!a`W)SkoD?fD>6;Ca2Cj82kQL1)nkcNt(fPos5meDZM4`Iw(p;NUzV=+(Ok3ookafr z*LqbC)c9&yrYsgr^JCr6AZcS!S1@aX=010YbGf3^3oi-2r{8Yd2UBG4LyIq&tY7<%}xK=;gc$_o6dUAzM4Rmyr=c7H>^x@`;6 zoIUTsew|IfeVwJhx&;}nZ;`Y*FyYCezQ4IkGXit!cTZl-^yux;lxa&(ACR+L^NZsa zqrN^b)>}J3USXq zzIm>?!c@&!lZz~?W<;B|nF%~Goc_df)|tsqE7HY3TP+k?ZGZaN@+psG%?g*xq_;OK zuW+clG&e%-=*C&~Zw_ubJ^xF{BbN_C5!@CU*4hr+P6k{E%r-bR|KvuupMQVf6>&S& zKk3W8y<7N2CM6~HE&ah=x_Z@y<_l%+r)$Gk&5vV#e@0z@28U&T%ZclseeHM5o%G}E z?(gfX*z@aau794N#!;Prq^q7qi*eqi^28hlr(CVX*$nhAv8IPK?DyQUApiWiOY?Yodk-pa=6K2ae}WDFYvqtlihkE~ zw_I0vn_&EKftEX8w`sZI!|=^-yz8?vwm31gZaFhE=ekPi*0dWQ<+?7DUDoekdrQE@ zNcPyH_1{ARySEsoo>_6r^xpx~v)8;d9`!j-5~z~9v?=;d<-FL?<2>8;E$m|4_Quu6 zq1?GTNy1L~+or$Hd$NN~`i<$$`dCv9UZ^Ha0cw^sjsuKrV#tx318`0anEUynA( zA4t--w>Mo^@}}}~dh!MblLM~y(R$rW3zUD~?N;ZXZ&$;$^y$&%pZ9!{KH<>UU*=dM z;&t5B@8Ihr4gDos+<)EXTKNBin7EJZYpvsVvU=(hML(W0d3KM}a8l;6*KTW;^xck_ zx+wkSvAFb~e?EUaTI9Cqxsmls<0g~WoaWDe{J5+$w|*USYt(~OZprvAt++W$9mI8` z_6S<73cKbXUbo%)`P9Dc2VWG{&71ehi{Y>9)1sjEjsj(w|83WHRGQ76mUqSY?TZYR z)Q9?a7Hp47++P2;#%tQz6dmopBl}#e#O;I=uXt_bUK^3Mi1puukN;c(-`#S$v}4|} zdyUtuu3u^TkQQPra$e!IiT=lrhw4sTsAk?VpP^CCWyfUk%`#^hL*kP@yS=d!edEBU z!=m+J-%{fR$&1fabUo;s_@UxKiK2gDvDlt3w^s#Z$?Nj$+kJKIntC@L56cj9@wycS zZHG=6iOEf`aGUx;)%*Nki^PMkqt@}sok{EGeOh|vMMp9J+Kt7NuUh`CW;)s!dV7-B znx(6GZ=|iAYMUQlW%xQOS#YI12Ya$J!=u8m*IO69+qUevvgWjGFTU@V_ba#5BsWUC zY|Px?u(e-3=I`rp5fdd};~y6F3JKrymT0vW>&Y>@9jXcR6M1)U!{g(hCMs+!n=M_h zD!=~9mYUp6E3cbp8Kyj9Th1P5ta9yBUpZTDuhJUXWCiP!**Pk68e2-2JMKEPtfzh7 zJpU&<)FVwTtLAMEm4uJdf#r_Dfi^iS)Z|%XhU+?PJY8xm0I_z#b z$2`Ej;%kfjjF{ z_I<+&3`m2eOC*q+ZeEPzs{p2T6${fA{TkO?H}oPy-?ei z5EL2NcX0AY)$p$`W;1v6&tToAeAtAMU*YG=tqdE4g=JQ35Gjo735zv#P>{6Aw%q>4 z$6u30ul6p3p5h@X$E}x?Eeh|xdYF54_S}^4rfNO)vVc09`e>%-Eo)a5sj%i^m@`|I-h=2Hb3lB^NpUJJDHixl^Y7FWM? zIdA*UBJgbWm04?Tn}WUSx#msb=yK&R2p2u~ib4OszN5?`uakM3ZmZsInRw-1VEx>y zMIQV%%Riid7x;!ppReqpCC`#Z;q?>_8s_Ie}3 zW}6c?ex>SVU;d?j%&P0^u;~+A9(^E!S z0p@w`PRIOM{&HT5T+*d{*r;OtD*%PcUEuF>DwoXTAO;64QPYw>QQ^^sF&mBJA zZ@4Sf{F~O7^7%7DAJjWf-B*8ND%;mkIoqkB8e;|1wC=Q!ndM`W9!N$k^i?{6XsO?J>J;WysXP6X~n5^p~>45 zUp*XPO7!1F4nA>KKU7=U46kB&pmf;N?v{K zb8%a#=!MH2FI}(h%XfLyW!P7L;VaMYqPx4kN5pg_h)YaYj6G=h%uHl|o~$$b8%B{Y zpQoS5o5%Rp%vfxa<-}Jy(euQgOTPU2a@O|WKPJw&n0(u7@2dHof3`j4;pxhlcPM^C zl8R7M#)C%Al)a~uERX)_JD9rTPkj*omOt0nGTIeZN(Yw+to~^H`odMyw~9#tcOE5} zcPXBkm$y7Sv8?R-u^*1Fc$eLdP#256a%JN*#hCQwo&WY$UphYV>%yQoey6P44$?f` z{S#K*3yJ@%FIKwmN|-ov*^TvSJNA^e%YHV~RJJ|*!AQH?`fT5! z!b?w9oLVS1`M?aPzYiKZ#68))X6B{zJiPGv^O_wiY}!)Fbl=sLGi_qOyYBe&Tl+2t zw0umSlK!91Z<9$xzuFA}h0De7t&bdFbD8KKw&H0?Q<#ua-t%4mm(GZtlfJL?>x*fh z>^aZe`T0@j_oVu>O14v8{SfRr<@NZ!es}j%72%oAsZ%c4XL!rhY(4Ph-NK5!Wyzmw znT*%0A83Lib*}v^=oH8x&1A&@Tc(t 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 delta 45756 zcmaF)gMH3#c6Rx04i3MKQjP3e*%{Xf*1zBUIOO05XK9{H|A*&pZr}SM_Ep86&wtTvIv;*>(T;WM z{%-YRnv)+|?0<9crJk6rTiZ7$t^AFLPB4!!~e@HdFTl4e{FDDTc3e|J&?|F8tXKn7mSkA5Yo3BPQ|NEm=qxhetn9C`p`}Y0( zTXwgExSa|%d{SfmbN9FA1M@l4!hW;vW8>QDbSnM0?S^f)>u2`t|9DRGE$_=oQ^a}i zd7u0BPt>lG^J=9)a=plh%6mpG#s9AvoXpvpc)nx0P0l9rUcM6}LT{8QKYm0V4 znposaF~7&(nf{ranlYV6@&2B7!Zw>%M~ZH7i3vP@{D-){wA`x5ztfWrUfjIF`= zNpfeWN?%qCV5nxAvu(@Adh<0mzORy(55B~i73GksX;u;;HH)eaL zaae6Q?Rrj}Eo##Ih^6l<+h*X+NE_6s-l5x&yVtRkN;MQBrcR0SA{_P7<7K&T*T5ju!t;Y|4y?fXBl-7Uq zXyxmR_!sP$5utHweZ_`Ld>qpjxJj?yD0Xc2{&it*jwz}AS~NQ;;>q-Pc0Ch{zI5iF4%ApwkPgi*Jiz5Uj=;5h-j(XR1}wN9A?OjTrAo?&{0_4w+E_Hzp!#F^Y%_n_Wl z&%@Nnr|Yku{xo6JlJtwySsooaAmDDAvY>vifZYqL4J#f@dV1nPBX_LPX|9T_yK}C6 zc&EQQ!zojzE_jY!$^Y5z!Hz3Wx-h5jUh&zkR`=8Sqc--c_1|}zGyJMwwf_2}@NK77 zoxW_@Zf@0@EvEOh^I%ibziB!5B|m)qby-Sw{o>;`aeE?7I@BI*pVuocB;4D*?)uH= zYZCgm?P|R7#@gn2pQ8Jr#>J*eM-CJ(S+e`Ot4~~Y#F-4u^-NsPTwS`I{o13r_1(|5#LSC3w3<6iyj3iz^{SBcc8`kn z70q!UF1suIHtWCO^qnF9=C1v{o2LhgG0IB$egELMB=KqqW7|7}FZZ3Fe$bCw8PX)@ zcS2-(hZv(`z2S+Jj|>y9YhU|(^{Y~OwAjM>W?!dQD{d z5&h2cr(*fBQR$zqT{`5dZ=)JrRuijLD`DW|^I%?A!`%nA=M#?H^m}l3C;P>|yhl7c zTMmCaXc6p};?HwB_1Gq(NTtPR1A1y_KJSi>&zvbSGfq6B`-1a{T=#~6XYx{)F zrnG$N+NS{CWHI&DV6d?P2^r-CBZCx}NvG z)TgZFD_5v^|IX0w;p$pa*0UXbslXqy*;?dX{Wu3@!7O+piw>Nm94Yw$VVx%+w_mxjQe>Y|4M$`0s zi(f1635C>Oxzo$2FuPl3-U)8yT`$U&dDLIAz0O%L_WI@KS3VJ6S#?`^h_>*Vwe4+rKTCk3D_rT2~EIrjC|!xs9KuOYZ9Q zPPvoxMKf*Et8d)$t->PGQ|q?w`S4W1{>g=e)~LtVKh(KB{t(aj&vNQqp0{FwQ~qA- zuyg)s=d$5QriZtrM$*uk&YoBdQSI@5v0$uJUf~jULsd0x6&wcdk zWf}843s(aX!AY)%7G_HPevp{7MXN0M#^#UiN#W^LCps@FpIq$Gd&2CH(e#(A4W|8_ zm9<#j_QsLo?liufv?(?x)OT+AWTM;W^7@dArey0e{kID1UhfeSe6wWliRqQRA!Dd<8&S{#fpMQSg z)spo!TQ=76wRV>-K5-~=fqSCw#FWX$SQ!=X`6~5hZoKhK?$zrvvO+VSM~4H*d4bvAqIrB&KeyR1w zSdri71sV=<#V_ud`l&d)wH@PD#Cbvn{f3wv)A7 zM%F&9#$CPY5gc116S7#Vdh%Ti=UCNGn6~1Q(|(49NprkbFLvp!%9^)&%9k10J(u^2 zI!#M5Fa_VKd3 z{7Q#S#{^c^NiCdP-@G|s*Vp<3o0s#iKQ?1c_^n$Dengu!%ZH%bY*@3OCTy>NNAukDxS84gk&#(G!z_KGonaoTyL zKQV9n%s&(C@5Oo^&9lDdeI;ve%6ivX=a+2aDB5wNK-*wNU5@U*bG_T^x27)t@lnQ> zUG#Dc3%^y>Z#CYMr!QN)cPPJ|?!dZ0`TEcFFL67*PSd}3`ct(2&j|^i9%bw0WJTl}9cr^qPo zW3Xph=Qo$Nm$N3Ath%B(&C2qnXLfGlVO_a4nODy_95}j}r)JoNM)w{$y3M2e!}C8o zcg@_fx5Z6OYrfg8;H#nUSH8IThs#`e!!*v>OQts}G73xj?AMsp7RSOPxTS3Q>QCLy zspCQ`-uH)z_`{{qKSL?i`XZTa!)pzv22g|MRQ*DHa(E8SZe~Kbh)r;a@#}y-Dok zzxq3yc0Xx+vM#%zcByOA(_eQ({SGahSM)yTL;dUU53BYZRMM0FF>jKGh|$cwbuIaC zzW&qwC?l4koVVq>%u}Z5Wsa$0);>@AcT{uk+0MyR|2{S6jem!HdydYx8PbaqRfMa} zDDZ9x9=Nv$hJ9b5})5{%E7vC@&Z-=_6l_&YZqZ_QF-eV)zfb( zG1}F;cq*S=bv@fbbN>_VA2X#27vJ5Z_)2!t@wV&wmkP@_9zMD#X1mB_t(f(Ri9y;y zYu<-vE2ovOe!M`^m|pqx z*SAId)|_7*=PX&B5X<8g6C!(A;qBe+m9e}39e&`p<3!l1=TA+IwnVR9?|sX>Yx2z~ zjp*8wd0ASYkKO2SIQV+m5{u8bbSK2WFc#N{{cW?`6&H(6$E+RV8` z*wlNfonF$Xz_PYLg#~@9f8}PCCTFNH8NXDy$G7pra>dl5oxx20E&&e@rCIN|VYI4N zA#M656-MQHskL+dtDd(%I6sv!{J&@WAKxQ0EVq=o{CaA{zE)({-G`aJ>f28VoKAAN zc`!n&a%YFSYVVrhvU%qPKFGTkf1G%)sZH3)_wAw(KZABP-prlR2??EdBeq(|E1flY zY=3<5WKX>x5h8QaB6O1G&1BxI^uf6x<8MN5i`49-0O40wlk3H{ZqZ<7c*uC5pk%}8 z(y9Uj)xT+y%b$Gb*E>8xdA_P%KBsh2?tC4ekLn!z>f>U=;|_GiNE;-`?0Ehrurp*` zTb!x#y&}B@JzWVkb8Q8co#$Nmo>QpEc-}T4X8rkTe9u<$H85-~$XnTQx>=6%b^Z3} zo-9>g9jB|4G7S2x{$8)&8k!TLBDDRuq4j#}y$iNlnY@oXzq@IoX4r{4dUjWzOn6tY z_W<7qt=f49<8C*-e03~FDf{H$v!qJx#Kh9g7F+19^*Z#nnC0r`MGS72L zt8Xu@j6M5#y2pRNgok;CQ{?srOy2z3*do?+f%`}8ndz@9{i0$ftgt_^>xAmbIR|cV zd93ugEjjJ?qNYt7PVG72o1poS$?fgs%DCwl)fgp$9v5$YH0kWJ$D)(vwm-Dg7I^ls zqUQ7apS|*`#((szZa(U3Jjm`@Ul9<$`O`l?JINJEyT7>xESw~&`6E;=BrKuaXybM>~rEm%3-TAfj<{%1r&W%OJ2%=a&v_>NnYwu!3bxJO+*pY+U7H)(?%TpJh*u{oYu>k-Nm_?Xx)=GhSJWBpPh~qtI(`Z&&Q$eFnW! zLb^9EyFS@_btgyZhvQ5Jj_X>?9DU}@m}gza;_@nQ(H?Ju1d0A#4Z7*9L9cH)fl6`yYOOuyl9DNN}sjp*J<@n)cej&d)YL<&ZgQ+@`zB{Z6>?s z9NUw}1*2m)nI6As`&Hb*clD#wKFu>l$D9`zyPkM!zqE)))x&91g~FVo`Q2yzwJjleqit^uli~?gh`{6Sw_Z@Q~@>vx|rLeFWYH z+?tx_X!KPhw!Y5e!9*PcR%;_iF@-rxi(YmwbT8P^!Vp`NI4!9taN4g$I-Scbz3V!o zHto^SJiS5s>L0HaC!UATXwcE~Iv{USS!-0m_{ChG$$0w_r4Q^sUG!hI34LTQ4gKSt z@%S+RR-dlpOU1oZUv5?THf`gSFK;DuwH`^71=#Ga5I@?U!+WznVU^~e2bluOK4+Th zf-4`j6lVVN^wzLQ+|&0+x#rmG*GtQfS&PW{=xxY+K(b0vjZ(nn)zs#kXZu7V7 z>T>=)vvx<#k-EOyoMm!CRoeH5DUu?Q0!!Jb|G;G;Dfbe>NNGjEhe*ACq+j@?BNgHzvF4$`AoW9U%&C1@GKjrg3XHAXWX%nWoX6vrd z8dqIqYn6|)l;Z{S;+!hGr7fNwy>lk0_Ep&ZiTZ65cJG|189!0>ZG?=!{F+bJ2ajZ@ z_RYU^NY^_&?EIhWO|>o^+v^sKJPG>9w#?N3@|5WnY0EbKvI(6Z@$$l_^BK38>b<|z zgdPy8U$EkVbCCZ<+2SA9^WR6dMBiJPEm<4>?JwIs_ub!YZms36Dq6|ccWSl$j#{3) zlV`clZvQS8aQa@2GrLJ}Lt0&Hbiu0Lg4v3n4Eh*U5>#fG*L-~UGe1B6UscJkSBD?& zzt|ste{V&nO-6;~!(@h9+YcNawe$Wb&6lokZY_+uBHwp!!{%RJGqop%HN?9AT66qp z_k_|T`?pPbre?0jeNFS`(cQ~8E)}$yuDp|d$BXl=q1Kl9;@aCM{7d}tGHvlhlL=~( zst0wwM5>QlY~Qd?r_*aYf16ywWrKpg7%J-5GKiL zq0zz&syIIu*w*}oZG)k2cOPHwSbNK}`haL0vnaRSuCz!4r zGkw?#)XpvBc-5-ymMo~H$#XSe&xK&$2>Z#?b2yzu#4sSE3bIDc&m{de=l_HUO;g}-j& ztIwOuU|B3Aaa?uot!tdqZTAFh*_ydwo5yLYbZChR#g?N_?@>L3pBJX2khSG=70GFC1Z zn6F7YT~d6Oy6RX;tZODq|NR<4)#&#vCP9q~`=SKg+*PDvZ?5b}&?wCEu5xZ!9`p2a zu*L^%S8i7y|Cx+j8h)R2pRz7o{b#;KeY$z$>ACAW+v0iHYcB8~S?PD=?5Swe||kJ-FX<>D}8;ZMHSaB-bji z$*a$d^g8l#LbGoedy!OO(L0^?9RXi;cjV|FFn_S6?i4d?OXTvTGglYXRrzsVePGi! z`D$>zl64|rv>s({@SpTTVFh}*i_+B z>nD%3F%yib|_(c4}rHJc09=TrHWyD!8II~klb8?6I{AK#L{~cQL zG0%7Q=_v+3XL4j2cXF`3{Iz+uAxm>bm9WLpqT6%-mDp|B=&Zb>L)>fzL)Hz!(`$na zlPVre-8@S?DX9A>n@Z4v`>GDYS9uQntEe@~?E5TyfK8l9Q7(LQ%_h6X$MPG)&%apU zGh>@o+&mtehK3`Vx9d~;K6$LDV^V)pGlBOAr}vM}@Bbf^MBesUrPNz`=$lmb`>D;V zK5%B*&bh{zV_$r1=dFhe-qdWY{G7xQwcKQbqvHx*J?1usA7bVQn%jfZ`LgfH=bev} z$l2&q$#;QeVNk~81>F07e%ctl%5Jvvnd^_|Brc-VYB z!W_Su>*?3rw2tr5Pg4tcKeBz!&QbHdoz{8NcD)zR=HiH_GmAGpowwq2<&+C^h2l^WhM0I9P%FGNjlI3{y z-siN z7O6jY|K-sSuXgQw5&n^_H1lOnyiEVgoNFsI=N!Ik)};CB=Bb%_nRdaY z<})7gR~oK3di#F-t?ew|`a93XJrsC!`*Vq+H54%ADgFaSfG`g$bedOMUMA@Fk1yi0i#d`KyemTb& z;#!mKzL$&Nf679EdP~3QB7Tc^pKg5d>O0GqJH{DnU+j9;;s04}R#ee#1D%Ll#WvGU z=30cM-&k>agW#iAx_VVt<(|G~o!KZeQ!{Y){LR4^-?isx-mqT#NJ38|GvOUj4QI(DW(SNdE_fd_gj9uuiw9Bcq~M*qkt;H1 zO}^OP8&sUgkrX=T*tUZnJ0_TTGqaSmtuje6<$uVyxBkR_xvA?DX8F$coW%6}@tn>w z&-U9pn`&fxCZAaw9=O-%p4C^^%Dqv4dNvv}1RRNYP&<{;H=LL4WSo8dw6KkTQ_k%B zbd9rJV9~XDFTZ_OiH3sL=CJMlIN#vT@>A!(X$nfM%A34~@wnaEWgE={^Ix$)o~+{D zY+jzF8MQ^dz~f$Mz5I()0%vbeZ+5U)6W-iqkx&AntZ)_TS}9slqR1G`MJ47 zDI^Su+#3-cK#$S}Ah zMO4ch-#E^qaQunZ&BwFaj)zt}zBOg-g-PmKr+TBhe`U3vGhLOkM2EG0uBl64v~)<$ z?bFAMyuViEPAc5t&%>P{D}4XNnx}94bT=5DYrg%cew$=^m-BP^P^qweZtFAhk7oT& zOZ>H{WSjRg^$L?0Vz1}Ty8V613FCVg!!M~_j@qW1-7;O*gZu#osHDR_; zs@^-BFoCzvua(`qyEX9nhrS0&8=5|7t?<~pzH6$UELZ3o!v$9}-Z5}X&(98dA|mcF zxxPU&lTYK&v>DG|f33D~{}wuh>4jW&EmPafJ1oL;4496;RGl3C$W}RA=<|&omlrtP zo_5i?gU*uXCm&|x$oyPj-fi}kNUpCECj{I;|*X*Wb{jJk8edSWqFR1t#RcvMJo%X%x zA;&}6S7H-q`~BVdQt4H2iO0UyitlofYh|R|n1s|+6(czg){PcTrk;`i!=i7J&$+S<_Um%FNq$!o?{2f|y51 zbmxbf(nwSL`wsP%P8<%K7lwB(3i}&bGqN9WdCx{ z^(v~ROwN(VdD&$gr)wEo?_2TdspqA`X;0oKb@G2+vYa>K!S)B|_r{3u9=+M2kv4fw zLBWkPeF4GEIxV&rzs6U@Z0Z*ID?R5(eW$9Q=Bxdpr3bGv+`8^{t6uH;xzwF3yDsZl z-kM{0=Wg#4zRuqZCTh%Gx9!3{t8|{gH9?&Bg|!~)-`^9`^+03J*JUaOAMQm2JlnbG z(BGNgw6Cb1n%BEbau0g}Z>vwst96Y^eMjCch*%^wY16s&8(NKpMZ*o(9qL{rc6vjC z!qpd@To)IAe#ggkequb^i@;cY)%qBH)t4tPd=2t?=ytc1?b`9DoN~=fssda0=yFFy zZIyDA*DA^FTHAT!$Ap_n^EU=<)S5N>Z|zdP+a9v{yQbb=|KW+8uVCL+zwCkqNu~k} z^X)>uueuScRQtSg<12H1xno(~vMLOlmxdqEXk5POl2o>4NT%Zog}hI%Z2o76pPR%~ z-!6Zms3H7aF6)ULE%nbm2|oX(`M>I3Q`dY*IOC=4s*?mNm`t~jgsQ-X0< zfl^S>)I|b{uTGxd-1bZTxpLX&a}2$uN7Pw9rB*7P^vLdbc>B zt#QBkZi9O3qYUki>s0ueFRqS0D>GkT_@m5uiEsNWgeUD>@Hp1zj^L)*dXq9= z1o;GA6XLV0bm=rxDQd&Oo)RC`0?+RI*%o0jBNXSsz^ktLcQUCZmaoz$-`|B6JE?uE{aih6F zdSlsSzRX|EIrwMk~J6kviCf+_HqrM<|`I``y>U0JpkFvv@t|}65w;eX# z>~JVgYf1FM2A3;`Y?W0v#!U9!!?$pSgpuumxzRWIKV;oYSvkMum}=*In<;O*v>tI= z@!Yu+x7LpIu!F&;GY^&u?A$`^+ki=neZyB|-!e_s%uD_ObrQ z*X2(C{wcP9TUG8e_ow>t-P~`|Y&921Y{`guB|b@fiHpyT4bvK%cj~?Bb}(!{bL&38 zxA0Qi_4__=^_=b_IdAQ{)pd8uHhZQg?~n_9?)1Uo?9o@Rz1BJZ2vB`EVg4WUs@%%Z znXghedGOo2h_Xyr8^BnwJ4-T^gMX%k*M{_^z4bTWOU?X{C2w()%|+_M1*3?y{}Q8i z7^-`(UdQrm(Ic;|R~E0~Q;}V%de`p6gRZhwt#|T^?i*~J=2_bByXmPc+m>hAoL=W_ zw@qud|GZ)2KL6*Ark$nDLfm@_Rpx!}E6Z+^aZ}%FZJL@abhC9yZbXv4FLTlMyqk>? zYcG8E*U}HHzZRz1aEH}fLgd3Og(bYfw>=GxJI$ZCWci*d{?g5#&zRiV{!As`B)fXT z=|o?#dwXJopQ;yLS9v(;c4Al3?-_p8tt|JXCr?{Fb#2Fk%?`^&%=S)InX_eLOU5+g z?9+3$Y@0r%fotN=)@>H0Jn_nFUv0ZrHqr0&lqs{{zq|Hm(d{WO>g5VF?qu&xm}FU# z^6J8~k7_TUe-oSQ&b@N=fz^nWbZpFK=`rgSBCUe&+!< zO|>oicW;|;r|y^kZF%uC|FWwSpB&#A-iv$xz_HFM7M;;!6h2CR(!A?8_{I$_J_ zrW#u>RbTZd`{I@>X2nfYUvkR6I8;7A_q+nH>Z|7qCL1oXyYx7ki^);-O`P!4kjm!z zOMfox`2RxCQT$uO8N0b3&zxR8`+Jr3RgbQewX6aXzZPuo>wf%9C3bTd(}i=dV{&$` zQz_-l@BU>e=J2gcS777s*Xt)M`CVJ1RFrZ4u`}Njzg*Y7ZH}A^<1!wtm3*U6UT;5% z*R^0io6fCn zJn+hKx%t!5tCz#{_U?c2`HQ%sidy*f%54W8sZLJ&u{onH@XU|pN4od=bVp`z9Y1p$LYo$+{wP3L5zuvHG8ZKoVEJJsT5UI-S{Rg~*E+n;#-9H;x^ z^71`}E;V0PY)z_HTfkg-!-ltz5|abnU8XMRqyPp;Mm}Og^FZ ziTA5E%jU{6agXL0wl_$e6Zg6|Wn1WLlcQgz=uGSHej(7hCQ2(~mdDnl=ogO;n@Y9Z zy0j)((#|OSXpivjXNBSxDG`siP7;)ozJ1}?BSAG*mBhT;i>A&EHmNTTlGII1U-R&& z(h64FP=f-8Y15odZG@h!I=oGtKUys>OkuOpT}?Jk$9qo%{DbYAvu8ImI>_I#v-HyV zXxaH(zpC%diqgxE9U=qmy($)XitsBRh?<>KzyHC~{jsx?=S|`4FJKZ_CuF>%g41qI z9EnA7XM zUU>H32$eW@XFt!3?Je1s0o{fV*7i(N+Bua`?>^_8tqloxu<bX&TzszD>~b%7mf;$C+)M%}sL+i+1sW7e*j4K2Kz zb}&0eIm6ks9cj-+mZ81vQ%)FQ0 zI>PMFt{5)Xa9=F;xyW6W(!N6`n+}VLf9~}?9?DkPd2XTzb77H;WzFFiDuFxSuVUK! zK-PWw)BSz*Z(O=cQbR>vI;>YRWe%wC^xE;^&t99EaZ0~L|2a3TS-b2Lb6RBUS~=$O z?P0zrCmA0rE>@8d`{CdB|ATe@u5=3v1|RkPRjnQ01=e~VKD)qYs?*j4)~a*olFFRh z)*YX6Ye!c2VonE!o4!^-k3Jve)@DpRnIt%0%=etY$&1x)dG$qey&Ctk#D=oHX9(&3 z!7zQAp6cD+B=Z$^>efjK{>pp)Za(E#tEx6>-<9H3JX5)L%!z*ze!(E>)i>pbulF9m zaPr-yY=(~Z#UJ^bFD^d#La}B6-vjTW>2vE{*Wb06eouTx{d*q4l+e0J?_JUwoF*%S zW2gU>5lB+iRJ*b#_H?LzJ;VEH&T;NmlRYg%v=8%fHpmG5UfrR%r0aG3=Jy+TU*`{P$wch4&k>1int>R$dot+>-Wr)r`+y`n=eU8`)pj|b_8hHB62 zOE0|@&+K9ILj1$~6d#2PHnNkOx7Ybu^8S%lf86}*cO9#}xsYTW^S7DHAHV#1`DcjL zmlyH%@@F2i^G^?sv~k z|L2^EJ2W2LTVvrq@hI1k*Cl@(B-_Qs+uG75wzt0q2iI(!uca}VU`h{R_ zbEA7z&)&rr1*&VGgsOkAtuhx}8rPM$SNd6eaYXD#1%aK{oy%elPGo9b_0~czE-kz^ zjHiF;^40!n%&j>IdR+bkfF%r;`t}%;nj0Uq<{mzyFcX33LB3 z$Ddr(_%+AYP;U|U;rjk5Osl*-YA!^)IBKzUx|?X%gzrI%)2Vzx15KM`J*`byT^*o3X3Py9RMoo3-5<$o6#8vfat=eTRp zsh}%ABJIKqXuzlFl)q6KhEu*R{L937J>ZSIl|DV6R`}?-pB$tc--!ES< zq5cSaheOw-wKg7yrH+@W+0DMOZ`!j>Bn6e zn^<=qKfd9{bU!ynea08l>)aSs8Bb4N;>M`Y^ssOGRX4^&rk)4WE!-I=>9~b{j|*CH z_)6)074fut#XC=}U-e1mak={Sv~R1qBj%^1m3{6}jZc4}DD^md`Zsq*V*PT5H&xVs3TFPqZux^-xvZs8Y{d*&DQ$>ELY5`D>>? z_Fz2B#D07F3QtBw#?q;7dxdJOQi_J7nz(DEW0$h_VQxp(?_x*?IHv_R`(<= zX0zT@U4HG;Tc@*&+ZV=8kM&}d=2_<2*PMF&iOFuQ9<>rjlKe!?vZ#vMxobSCz z(NMu`x`HpG2ovj@>1Mu+7L3Z%^L-hUlwH>f`tO=qv-Vbq-W-|5^XCPda3^oy@KE#C zxk+Zn)^0F8GGov5FTRY?OuVks1N|5qn7H0ezwF0o%y?uvyFcT3#vRkI`!lX%n)r2k zdjMkvlVQ_z#z4jv?qh#L?;G9QuttGNWcq|aMoUQ@$D0jC2RWKEVZ6!A-g-Q~uI{(~_J{;V z9%e@F>8Z(#rx;zg8>BGCurW@Y-kZf}!#HvJsVv5wjNiAnWHXjCGgfZ@lgGG-gVBBa z{Bp*2CdT0D>Q#&znd}X=->PC%U}Q|){=1rS2_xh8?F(ud_b@UxPmir<^k&STzOtTC zkuiPx>3YVz`urN_@4wgbEfzJE_gm8P)MD36z16$ccr3A-H?8;w=WRx<(@M2np?;PZ zXDti;nUQ*OWB#>J!PG^e-fPyazACPn@?~@7x5lTl**CjBX>8@tI=}FVJ3E8m6)V@L zN0P3Yt6#J2+qX{8c_&w&f9c)w*^%6=nwzFHY-`@7UXa$eIIg;WTUf+z!FQK7^2OP| zdY{bn?~uH#Z*gq?>qR$f7e`LKa=(1np>^T&O$$QKZ}xiE>pF9BpTqCBE>pOx3_h#H zIyhC<*`4okpYm;Y=bFoP<{Q89Ze4$W){G9{rrFZ}wnnkG^+Yx(%l;PFKr=X!4#U)e7fn#lNw^VcB{@lXCwv}_(eJ!GVk?${*8 zs$2W)--ev%e@#nSFW-GWt#FEknT~!@$Ht1t2@Bd61boRfHkh*Rx6a~&U+pb5>$#gf zE0v6x?oC!{xODqi#p2(O`>U?K+jCiY{)L0Je;)nx@4s(X|MSt4ljrZ-))#%cyZQh7 zpPR2QPS|DBy0(1Q-+71U+V7C(-)Gyy-u|&pQT6mwrBO7Wz(;$OFwy% zclMr2iSlKO-ON+$xUZiv2z&U6FJV1X<*`t6Bl~QrTlO-?BBPSa%s$93eb;wO@`SOYVg?BP2nl2|1sZaH=NpKFx65brr4PB)x;?o#&o!){1%AJ^P7&)yFiU)sGp!dU)5XKMm$eTi`P>WpgxM2vq`v;`KP@Ts zOuPTY)K?(=A+irTX?Daj_9b5kh zUAHKHvYXH2Ps=3t?Iyyj(&`WVjJ~y?b!EC#cdGam>s35fsT1GJPLE<0F8zP9ec|HS zd^TH4ZhV$IArNBqwetUy1-C!+i*@kCDbA>8n({?MM8VSx@5&Y%0VuZe2@R>)ppsJdRlrIf{yWJzy9<^Orthw za_Ljv$c_~e4(9J0&7U9I5HJ4OXRR}1_X_TB&tGPmy+1tV(y_ITGQZ3ZEZ8peJ@8)R zY7?nG*6epVUw*CI< z>CMPGRt3ZNv8)+PYwM@H_#-$m;5AR(u4~)*KbM@$`_ZsHbiZ}!A;VR>6ms`|nipSS zyp;Ey`2Jhv+4pZGiR*qpc=v};nexWeG~LE?8_boRL_Y~(1Yxm@?O*#8$S$(6D3g6G% zuB{Ef{_8C_cT3qay<2YX1Hsjq$0BWIIkz6uGn0Ea^RiHe<;jzeg{PG)VR)+$dG+j* zXsZv;>O@aV&&ph}_ub`}X3b9{YM#t+vY+9rVSR5+WwJ_p*~%B|CrDIS{y)z?E9u6G zH`1=#Q@`5^r?Ary4Yo7hM8TkL;3u&`H z-oCCc`O({~z!F$ruD+BD&ZdTuWz4ercRWnmL`eNdlXDJ-e~O*wJzZy{`q?B;P=c3>FZ4ioZg9NUHS40K3r8d znpgh7e0z%V_2TJrnG?=Fso%Wg74O-v8L#~;vIMhD)vV0cMHe=&@=VEA;LuyWH{sXb z@9U1ul+9l?m&KB^URm|wp1oW;-bR9VXGd*2**pK7rud_ss(4< zdmdZeTfppk{CV&P-LNl@qUXw1n`d%+#y)GUn7_$Pve)PNQT~Kz_oxY1Z~xCK+-g)+ z%o^Hq`t{S7tOW`aFBl}1EZVrJ(9O=;+FDwAlDm6T);E*HmCp)wp48PpGTzkEWc^ds zZLtUU>Q%M(JJ{YFIkBrXN+9q$zj%j#V`-MG#p;xl-q=`=jqY>TlwMl7aP2yc_X1mV zu1YX1j=yMI?^3>V_lnfUf402+ew&RQK9~Jg`j}@vy-+*1xHGq~c&79F&lBuI?{xb9 z-MIMGJuA782m4E76tcEmxxuQGUq4IHCu;X$yE$?ik_jJU+1GX-X#T_LWcTF0)0E?@ zbu+I>3Fg($3NsdSpX<@MWFue9vbeZMYj^xqY~S#xOMC9J&7a@2J<0i#ldSQ5;n&5{*vZ=_pK)>Xhr7N6&rpmKFRW5M&90}Z+dOT*9TYISr5Nk z{Oy^s>#|>CfjrQn5^5K5uV*!nPM4+*jTNd96G4&Y*XyMCGAw z6FH67$DcFyY8SCzdK679dwg(U9#P8@D$wnIpxopqR@a-o+8etpe4o*ZsVP%d&CYqKn`4HL34MoH@DYc zTD_xv?&2E$!|p!u!Rl|{s@eWKd--(hl>1*ad8N9(SxDcX8!B}wKP2-$TiWbvdjd*% z-LEa07;$v#jD3xPi?m&`!fUqQVN>hgvdVap$l{tz_csPJ=G&M!)r(K9Qp!^0kxrJ| z)s%aaQIr3B|E9eK_cb74cE-A}>pR`1V} zQ+;nNH*QNdpX~1B-nvtKUB+~T~wQ9Fu*|7x^Q z#HJt9w(m_a=4)HA|Gi?3?D~55=pxDaCj)IJ-jdLBSK-d6#yV92~EYk!t5s~=fxM|Nnui?LW z{qOI?3$HLpPI%_sw@zSDT(;RhqqUs(G>%FI@~qHkJiXpMOzxI#Z|%~Ob<1^^)*pR2 zcjfW~8;QDL_s8}NInQTAY~P~a|Mr;J+)pdoW<@6dE|&UouvaA4{G5dMQD^qJ^3}V} z8h+TkeC^wd`Y%gcB3k5(BOY&^xmL{0RN>g0`40`GeqXYC_PMg&>&vuzWotQeY!YPR z4;Js*w5Y!0_-W=w-`rJF|I)sHU1`c9z1Nz*KGxI1>UqK0;16F)zk9B{ckS#sC(njI zYvXPQZz+7_`&@0&_ebYqQ=XnOn^JcE#4F|*$5uU`J3lt3%C=QfQ>Q4v-B#qw0@>`w zb;+}G{!IV=p)+Ahc=r?@R>$uzQx=puT|0M=CF)D#y@a3p#rD6kqrNhk6ixiy2a|wKDIg2*Ym#**tM5$?ds#(epvlbldCBe;D1*y z%fIh#f0)Ig*`_b9N#050I4K&*z3kxicD@CCe2f9#-^V>a$52rqz|Y9?@xknjuWw_2 zJyu-TK7IeWSNYQ0E`Ph7vHR`j%G-YyzTI@z>}Fc^oz&=@-LiJ2_0o4ze{PTcHjTBo zTacq9?zIqK)ftCczpQoQE51Gz;|x_zl8h`lq2IsD`*m5s&$ZuA_6FIuUwl)wWXlu# zbyf3?>()209?f{R#>^`*pdcfzQkE;fElW1q)Yd$EVbaVUq3@R%rklNvxWuw9z(aR` z(66GMr{*aeI0a7pKQn9_t3*9Z+q>C;(Z@TvZ5>~lbP1fjd3@%UorPvRT@6?2!e6J| zunE;kJh;5jj^|+BR`%m5<{eENd(@Xid8BY9U)ZiC+iV(lXyyXd@0xC%2P)nLIQ`$C z=`xqQAp1sP??i`9+Y&SmTga@vq9vr5QoCptPxF_bMp}{%6ThrG6Dzw+DEU8Q7!~;3pw=_QpM5LFdy_$B(b-`?7{R_;3B)=~3H-Z!dk9k@Pfmxvt$L#q`aqY>!&a zJGAU&y{*fmGG^nq*;k%=%q@Fgefw+C`F+>^=9KwQQ(E`^;4i&xGo00{@4m}O%~sWr zZMm9$XWF#2+n#P~`_v)OtMrWP{o!M;-hQ)rEaY^l`Dmy}B>(o1{Qm{rRn>c$L$_Y4 z&5PY`s$aj*>xS(|N3+wOiLZ~<%(2U;Tz+uLRfc!Ly6IO9y!~Gn@kMn0d8Dy=`@$_v zlb7YMQ>kglpU^+Y&Gg7Nwi(mTZ=8{{;lJ-)XGz8H1_`3q4zBS!f8N$s>gvfq=hwIh zwH{Q`U-Yi9uK4)TpL{o@R(^NUwl?{GE6U$Cgc-`~%-V$ZbY*`+nNUKyO5a_iIG z%4@T>PcS^nXJWHxdEQz6S&chxJec0V`19%}n_Ssn-_EBWUTF30f9RCu^GXFCJi2`Q zP)zNuC4Y0n+K*R=+ftf&3 z=t=yxY{H(Eo6aWM96uq?B)fUB0rQ#;W~eMIccTF;be{}p}(Z?B5&j=pvN&Y@r7+jFy4s+;TyxVAU`W9;Q!hCQaW^;e?q z^Jzxzu(!zB`zT=14!!GUyT0xUeqeB4cGK5g#oS37r+nJsT(^^1#`EaeTsAI)H+N#S zTRucZtXRjxbK7;phAP=N0n#t;U61&#&HT3R>EYeBKld+uUb#*yaI3}}dy6@#&jb6f zUlHNHrC)wq z>u%9QpTfc&|Gz%cdiQw0bohqV_sptqs)+^M)f2g8S$^ko{7T!}{|9bfZtL1paqJa~ z-OJxbcLFckNo{5-_*Z>QJWRak^}WykeE0noGHS4zxaw+N_16E1pdfT&NSKhI&bJX~EYuEkQbqTY(lXg$D5_|iPC!R0q z&;7dWwQGEAfBtU_U-H!c+2xL{--5d~mokOzdbVAtr?E3$|HEI+DHFP@xu>iWo-6(R z)9KSW)4Y3a6bqjhOMF*(D*fF=k#DC{pTN#}<~rLVSH^j#1?@at|KrZr=?kAT@)Tc4 zzFlaQ$=PjH7!twxFX89uFJ1dCY5#Fz^$24*FE;HvZ*^8kNaw^S3vPI~D_e-J^9y)w z&FPmAc1Sw!V!V^T#95W>g4r9{?{RIDthB1^-p{nuLT~qkd>`G=Z-01})wwAj*jlhK zvv^|k%R}9pvZJ~7``kO0Wteq~$&Jgsc zd6UND+A`g1jaEO_zxk!Pw)OQp)eOr7L$MlZW0{W}A3XJPukUn7)Smaq*4^x5eOUbW zimvq3#qM`P_nhd~^~(J&^7{Lh+pAXBPC1c#?QP7ps872!ZY(bQ@9(r}%Si!;4=d-s z-*N2nMwhSAqSv+`3_Gg7_MzRnRbOt+lDR%h=65f<0DlD!$B$o0)=d@`3zS>+f=Lu0CPUPz=q9%^K>33RTX`Gw`SYw z@WM5HJl9l3nXczZo(^wWa09@96jCVLLsEPhjvuA#L1 zdl=bNs-uy?{|GJTXAPt z9rVad^tr$P;1q$GY7TGT9}np=S`+lY#7iK~_*0{R%jI1+GA?fxv{Mr+X~>q?pzhKY zr9W+#^bPfANpoV3{tFdaTJNIQ@J{RHZ=*}vkq;NE-@j$!R};81@XY5AAxkDYjAiZ#gNUb?>!A`RjHcAD4D(K3=yj{(Pt`+i~^qrni_r8Yhcw4^KW{>cm z)i#`0&Gx)9Zt8u))|LHOE1uq9JENO-W22o|VdA9vZ{Oa?EL?O+rFt!TWh>+4gWwTZPy%gv;2#-*vOHg_$xqtmvYSGiu|eb0qqZzc1S zrt_V*RIKA%o9}x$WIFbn?NWW1)m|eI_~eXWi@Dd{t3{{W{wT3mvQ^EW=`+v3P)Z|L zrqb`l;d+;uyxeKs`&t=%4MWAwncYz2PPTy=UW%dIo(>a@=@GG zb}onNCrVOV1fTyrk#=?6mTmekwbO2Y^SN{8*gDlEp}X(KN+0SkJHN=c(CWu#X60Fn zCiPDHdqFoq!}r@Pm#uSecd735`f|%|wzP_z%;T$Xy5>k1)UU{rXels%Ts;3mNAi5p zubYIXstV{|ic(WxtxDkwo3P&LKa1Xki@RdJJw}8wJ}V|%4&MLc>(ihLfo(PqXKY$4{rPCmouHpFb2GOtn0LwR?MAr( zg}s}f9F4sBat>?Tt@#t(>-o#n&fc^2S+5%rdrG-7Bl~HOQ&Gj?c`6r-yS10bB;EL^qE97Nr~U9xm-rVlA-G$$=-YIU^-EqbU%wsh;*nId`mOl8H!7>)dW}H0{Y5A{8k2~vqQsc*O z%@;Q8txw$=RgpZ+G&(SEbBKK0{>YA}IWxB2Nn=!c_kQ)W9@gy^=7+y-sWlZSVXRhq ze?XUOeNJ1le(}C%8l92i_v)KYr_S}C*^$3h|H>pkfv|o5f+H^Ol4L#G{%g^n9lpwm z&$OAsdEfBN^LumF|FzKK#;uz-xyDW3%;RxC$DFe&zv*uMxnH{v8t1ReP-4pHUz6kD zt1fZr)rr|wTur+A3#(=^rv81uq~3LnqV@}ZZgr7);u`n9WL8bbvYW|yl*Kakr zXguWS+kZ~)zpzH8;0pIkJB;?6k#738__+T)yZ@|yiZhi8~`T343)+E_} z_}}2Hedy@_YQDGiKN5dU?msMlvmt5bzg4?acJKXUXwBO9_OJZ+$uw%g^E z2rFBaNuDeG-gf)g#ZS+ARcs8F@9oyI{(Z;k@E2xj!S|L2PrkHqebJTMXOL7H!Sri! zp8IM3@0a}VMHKn|m$~xHZPuj5WpgSVV{iCzyf43US&Z-Py}kVhWT(no#!PV66ZJl^ zw)pVvSv$Vi|D8TDec3lD&m&6?NtQ9aso#}h{w|YE=tj!o1%Ex-{Y$t+ZX7w!yYEu* z9eu&qFVd?XCqImp5y-w-|LMoq%XJ%Ghb{4&&!BMc&;sYu^A6_C%l=d{3RF%?nzC|6 z;-wdv*Q8W_h;3yn^^oCMpgJSEgIV~M|K8XrbI$Y2r?Fi-RWo;y_XY#sTXBoe8g?F@ z^J6n}{g?aJ-@YP);dx9pZwo4$M7Jc_s{g_v+=2YR!Ya8LckGh$0}qc6G1=D%OJZ|y?= zqC(ZB2L0J<|uf8JImGDA(c2=D3 z^GiP&j_FPpU%Aia+0Wj$(rkwpoGxfGFLO~ByvF%WMy7Y?WAnR}Z%*l*Wzf98CH6u@ z%FY8$ws!uWANq6n*v(%5W%&Q*Mtwy{yHa#<`-OG0Q>Uvc9oe4h&olGZYuk0%7WSDJ zkL=Bg?2(%nCUHD%+DSQ=hYv0)#oq}J4ScUU)pR4@w~Md8Ppo^ZU(C~By!qV(`Au4{ zmwwnUWxaOOu9&dPYu=fr@;gtQ6qjr!Z4+~D`}|XfEA?*j-3a&Hyu!V@+G5i_sap#A z_1_oNhM2zUwoAyqDg2(XKH%`fG%@Def3B3xH!k656(9>@lu8 zk$$syFZa=x=}%*;7QR-TmH0cXSZzapRSUCRq_K3&^6ABeo2oMnC%4sI2vIvVdq(WE zkZ#i-!kp{AiEf(sB8&OR`FDKb((Cj;$9~tdnJ;W9yfY%fusZu_=Nz@tnvZ*bUMyS4 zwe@<_!p9dMFR0JG-+%8s*Z$Qzww+h)vD1iZIo+e@zd2L6TTZmc=4Nk+SK|4D1!+k? zcqW&d+?(AVUz=$4%3xDfbAHy%Ukfx6SIFm7SC|^!p1CuS=|#)uoGojfDHihlEa?w= zWgBxrxcIEU-+A`#M|X-`kK~-^*!3)aZeCYTl?z|wnZ5nLH~PP?zbNGP?2n+r+Q?sA zb-UL${>?aWAmX);v-PH6zIS%HoyW_YEZ_P|&D*(7$wzVB%C58>>z~cNUaOG$bWO^? z3p4V(95?(4Vm;?!na1N3{CeIAjqZt}Ps6m;D_gg2UEb~5yPR>^@eA37ZHJzoY-axzC{n-v7OQQb>C?bzukWr)ZMpKz z?Wu^I%P#2&akWdc#cnuT^-Pl4IC+=oBgyTBJ0G4o6Eng0_S)hz$DVAranfyYS?yUL zS99XE%wO?47y4`Z0-i*l?$Eee=Va_!b^jvU^lt(^M?w#{cF+9PU^S`Y>}UV}C*CrX zw)%^@6kM10kHn4Gm_=FN4dZe^xwudzHMpPQpN{iCf^)_VS`&Z-~QoO5dDHCMRYm8tM3$W1S;!jau+hKyANQquU-U;NpeM8RylexQf_s66h^>_O7&GYw{cl>wa+7h)n3#0!BJD%IdlA)QuZ_-nq zqrQjZ984FlWfglncblVQ$=`b}(-l6yan9Nh>37DreusZ@Uz|7dJtGaqbFZIoSfH`z zX?n<^8;2IS$6K7A*f!nC-<{F@#S{HcnVZ*aTFj7HAJTs(M$EZhhGG69)|c0KA5UIt z+^eekEHPVdV#xiJO}xBW+a=h~%+vCWV^}rw)|!26x1?5XdTz#l=u@@*sqgHkmqyCI zHR0mi@bBCSU&c*3W#3orcda}8gOl|`)C-x&t#@m08yYQKTeGD76PM`r{vUEj=5JWm z@}yxSr&~y?$?nRj1xYa?w+_}{y`FVU{HgoJ{par$yI+YrDz!UzxyVw%RK-F~sn=Tr zZ*V70fvG?mOH4%UfObgYWb_>g>t8SJiEhpIPs| zSdgRf^qzHrsn0H6ex6@f_3xRl_rj0CtxegC|2xC=%D*OaPI&oA`{|2=95*;$NF38n z3tqu~`Qy6R{kDEREJ;e;HzSS~XPgV&GG(1?lwVN~ql&fj>xG8K-iN)e860;E3r!Ve zvwwPdmCO9FCkI|8M;&gx{N>q|a}0S0H*OGiuV31<`)K~nz+|TKD#?3$G zy6$ZUUJn#8lrj<@0AxMJK)fzFa&ta(9~ZjL#;ym-MUy zX6Co;*gm=bOUk7eYI|p|y2{`G`(U2c46a(O=+-QA1=TeQe&&hpPE}@7CUUj^HYmM| zw957RB=>T&W66?dZ6~hO@TW;WFxPROJ2(AnuUJWK@ekMAvke|S{`pC0U7f< zXSL^F*cP>IDfeabr@FIC7gYrCR7r(;U01K$C8X)|`r^5J_t#~vT6pWo?DJWHxf31k z{ua7u@pNPHMy9ggf}OU<^0Y5)^kiUUUgYP_pEP-~{M~+Q z4*oCg5o+eY1h=kn``etD{_)>G#n+7o*C)<7wBN9L%SyMyJL1F+n4UM-u5mGQqV}r|+@U|C5sw=kO-zoWJzaH6u8;=3SxG(ZC2^vD3epj2_jP zYpNN2w~%VxI&o|Fyfq74bgimcmK%CrJUY!w^4{)pwTnMeD$h*)`R7cMr$#T+N?EBB z%mLA_<~@C@D>GMP-Ja(Aiu0NruGXz9mt8k;$AJpH zNwY(1rrmikwOcRm=QJ7LzN?z^W!68+ZrFQ+(fJ0Ws_tGXlm3b#nWNYL6iaxny?Bcy zPD;*g?g!(kwoL1l+)W)G@mx<%cYDIjQ@={xHbSZJwB0I?iAHMfzWJN}^DSmnICiY1 zbsyUpDl}MJc0Mp+-o*?_G3nz{4>^Of=e+Q}*jbYB`{d%*(8j3|t*`5y%ENv% zy-n1dVG+H)?QsErTOaQQo2KgmInCxV&+a3o1oBw+A`~CiMQQm1MRxJD-=zDE@TXT!tt%hlzEkBCftc;uF zt-9&lq-pKDqV%n;o6j|x{pk=nCHbmjQ9Y03j5$)C?-!I$i+|YMzvAkFqr8h%R(&?o zo)%=l^2IZ>&V_4o{lt|_GKE?yrZ?-F?sxq#$uiD7E$0^C(6ra`lJ=g^hlkkP`a5(= z<}Byczh-y=}?=>^yHlz!Ezxr}(&dH9~bFb%1?cg`O zA?E+4zE8hU${!#s0#I<_j-Ga-~Z7x`TYJ zb(}vf;F(|biRp-dX+7KJ_p?j{y)T$wVepYVn6&rx?zh*>IQ~R`t+xp{b9edn>093) z$t~Z$optsFgU8JhOd<7O`1p;_evjF{cI}S$yOex0D)p0()L#&YI3T?yd3mp$-?a}f zCDu6KP4nYEk+(0x?_v2_WV6xZyK?|0|kUedKIwEpS^V=f`L zpuEDDcUQjjG-J3_GwbEF`h(}q|5(jYym)$%bnJspk&+N@x<4U`@J`F6t7T{Hl5g!&$E5I zqr(E_l(eJ4JA>_gSgO)JXV(d<&)CXR-=^JsaiL&UO7v{UJB;1wP6->F947C+vrYf> zw}<{#m(Hf!ecSf){%5P1cjWmVuYXW~|Iq(${B4IHA57h~LG65y|IZ`S4n_Xsd^#^t z;LuCq^Q?Ol%$@Il4qPDdn`!Rf{~CK#T4bBM9vzw(w~+bi%hvQ|7I(}pb3H!YqWr73 zcI_0d`i7Zr>}Gi7pS>|RTRd~_^Sez88M#IHm#(j`oWHVx`FG(p{+qX^8157J?D@xU z+T2+hIsIu8;-}nMVxtZnsa43Dcr+l#=JO(pO`XNI7xt?3|2{kY=JdpC_8n#14vvRT zTQV%(!xFd8E8*#5qe`yooPa6v&Ltf2C*)4K&QXipKP|ZaiR`Z(C-gZ*ZRR`v_KGqW zWsaZKdvg9$YA$V=GUK_%23@x1z(mck$n|{R{KENdG=lhw; z?C<+k?VV~{q>ukNlx}77E2rYaS#$sI$7Ro5Q~3YyasK{#gLmc2&Yz5_KX~%*yMLPx zzn|}*=b1C-jPWn~6F$p-oLRwYbHC15M#{GGt=x{Z+tJ5gY;LaolmDh%)%w?)*UW$T z)wRBUJbUM|$2a5G&ax_6pQ@R!r+Q5=Fyf)m z8+pF=#plzO^UK}&UzO9IzH|;d`+b>twihg)hfWinV`}retn5ecxn1X;t>AR5Oy~EL z{~&fvF`3`c_UQjJB^hrSul;zn`ts%VC49XP82c4$Z7#2l39HyzeC=lBudfHglT+gk zZ4RkdOK$$#T-g4SGv(g@v^%M{Sa-JbAFDG~IPv>gcIhqaUsGnj^$T6L{_^_h2$z}1 zdD~X+_iX&~`^_&N{bRe-H+(w#q5B=fgMA;~6fiGXn^?Z|cl-UMFYgP(k7a$`{=XxT zC*}U8-jGRr$`dbJuc)^CV96SD(Duu%31N?Oxg@!abVcjc7cNiPND1q>8mR4 zSsa%bt$jI%Q<&#&*yr2lr)_X>@Y7|KC=gIg)|%15|HU@E-E#5rToW#%r&Slve$(2# zVpWfGkEi+EZ2=p!XN6j=f8t>qpvN(FR@)8swjU1b*Q>e-7#wH+_5ZKhk{8+EmgL&z zPuJg)m^l5eq}#=gE#hZhGGwiptu9{wvqnwAZ|1o}5~p6SXi(xWf4}vqoKc5}m&Tbg zrMm)}E$J*PkCeEo3asGV>f@-j@u>dmFH=<88UIFpzmTw@yeq3wz3tqlz%uXq8*cq( zu$@>wE9Z4vv`*Zgn}3=Aoc}18eMR8txo;o$XY>eJXFl>fb6OYQ`xZ+d{@59(aJ}(#z@Z|I`q>88Wo-*wPu)N8@7GK|>oiLf z4w=$}=Pn2DNRYpKXMs(F$&r^%SN%l)`&ZvzboW@H>JRO+eQO!Qn3+;N0~s1+A}1yL z>73L)dGgF98xQV`NwSR@I=eI{Ei5=!ced_E-N}3IdspwesF3MjmX_A1yl9u)_Po2h zN?%{AeX+mC_d#_1*H3!td#4uPdgYrgw{eoZOhdzWF+Zjo?hhA>8B}gq!S(yk!^53B zx6M4WFQVrB)vdy7ruEO8T)6-1x`TfM+=^Fd$@aX~{xr$y>P(LLB30SH&3>274T&=o zyU5zbe3Y9@@PFAyu_y66yf>+BQY#P)sjrI@ol>&iY{x{=0}*d$T$;YfGd&7!HkA0H}pUg-Fp?=kJ>n|zT) zJnM{%l6AyCYe?|QAFMajKXvA$t;WO-wYso_=PLI6P6+t#W8|6I?^4+5>2tokeEmFq zu{rB^>rXu`liI`NTjKJ;)q7WmcYV~73L7^T@#BJXU6XH#O6bn$Q9Q}W{nmH0-fET9 zPqQn8%D*$e`2Dy2vX0P^8quTs_s8G;Z(GIeEU9{6K|}PVuUzLA|9ijNS&AXuQtu(3Uj5t}`BXW{TB~_S1WHKP59)D<*$5zG+dM>p9y*XxjG|k-I$nWolbAxK|bPRILAh zadEBvovX)Jew=ws$VqJygW8TKo2SP9>NKcka5}hWzq#u(|5uG?Hc2h9+qU}_+wzL+ z)6>@c%X%@t=wSWqg_&GAhgO^sNLZHN^XYnIB-5K`wt|-1B3a*e9!{`1TlePYy_#*JV>|)6A18QqRt@58I*eu*- zbjj|o$0E+Etyh+n&xO-^Brnl*H+25ozom-pVB-Fkmh;_f}!TM|KPraPKS$n(e z^jW(9W`!@mfADSEwnN+FboLu~8O+^g7|mNe?}O##lSg&`&lmi)Br#@3j{3oOSJ^kK zmuY0YTK90a&&+!b3#9^@mYUw3d17aVOZU~r&V%dx9`zk(Hs~y$vb~S#%(`=HzM88D zh33Bzul-rH*Y8h~S>7SNSw6F2DO)MQEcZIh(S1Y5ybspT6`miIV)`J#SGh5Fw!pQUH@uU$$~@n- z9}=jG&gi~!;NLOd>swNo@7vs3&>`qBeYvKc=OxFeZC6frEc=v@y?UXbdCs2Y?o*hY z>$MYfV!Pkm3q60u{#?6>SyteiWocc#Z9h6+3BOLQ()tjT|NZ0{e}f~wY zy&og3c*=0{aSwL!i@&%QGqx#+hL*?(t-mJlg6C@Bifx-^!JqP((~`NQxkNI4=w-GKPA1MtK-&Dvs`JR_gDQGPxzbLuATK^ zf1}0PzvuHm{aLo~R#xymJFHBcH^0i5TwdW&slvLp;hV;RDv{`_$)en;A>ZD~ z$gkgWCfg(UqtasSIV)~Ho}J+%H0|o~?|%{>=XS22w`ri8set8e}5vRupEyQ`nXg=yW6*>#Yy?;+bILrIC4HK}`2 zCD;A>zF&ST&sv@P)18-h<~A+fdYqe)+ws!$0!HSkTFGpIn|)ghY}-}`88>L{`FJ+* zP4=Dm+v=_D$?GDImzCOIcv+Y?!>Qw0z%!d2!I$isQ&LpiV@1;R>Z9HB*C+?Qes^qs z{8{5G6HS(M8$S>};e1W;Uy|&K%v-NlIP5nPh^uZrE%x-M*5tR>J)^Elgj#n=)lTs~ zyXJCe?_1sQ)B`!id4Vr@&!knqF_o0A*7Zy2(Q0MLW!Ymc5D>Zc>o*Qly}tg&{yz>Z zf7h(b|0nb6&i(9OfqVIE3HkMV7?~VqbQbXEiuLyXU8Em>Cp~!2q6ITQ%uZ7K+MLF` z(%t;@uUB_+{;hgvE&b%zyL%N1>zLIQY@FoRPpU<)A5SF&d2H|8)rRpd3(id z$^DhQcFA#h|3vj7PRoDp*Pd|w%Gc+&QaFnL-Hpvsd0ffhXkWv*gJSF2pL>n_bgyf7_UtT=wIzpZfMK z7F_Sw2PdeUII%01v140@;$z8K&$RodL>!k}Wb*4*_RYuF3eOVz3j_^E6>xT5AJ+<;i$}$ScwfW zzQ;G@)lT@udAcoMT=8~8#gScRS2YE$z3RG~R}h%8JbvE_sTG!zp~o0IGw(E1*Sl}3 zWnZak=#;joZ^C(5`zy-=>(9RMmv<`LnXhs*^v%NwuAbsbv(}uSXnSe$o5_~y&q5C` z?a(cAo97$!kiGrl&q;ARX0kL18lG(MiJUU`{A$CWHd#e4rhl%PwekGaI<-4CD}G#- znWCKLwv}bKUr4*$nk?_%eN87Ed){XG9phUtwc6I&9K0?z0HoVpy`W;PZ@h z^_O2(xzC8a%R5K$w#3YUw~rnzI!JHX-tXhDXKxOeHtE-vyADbRHm|F?62Jb`d7~2Z zf_1Zb6%*3aeq2{^KWl&gzu7I8^K%YtH2r2GlMufm(F z(VCzyXJQK(;(i`plh)R-FA;;=|QFOzH znD4t6Twk+hk-X)s*vs*EfA9W~=pVNDz@=5qDQ0pit`~03Ig+qow&m@KqB#d%xG4J^ zXIUbuqb@SVAlS{bc#Gtsz5VvzG&R?oA78?xDRs<8>_wpND`k$@UF>};XZQV7#{I$NI;=`{nA4y;a^z#x1sF$yqFa@y})%F)bM-4~_Hs#anjk zgr#Si_S8$vo6GqAMcMV}yKCRZ>Kg8yRLJ`9ouSwK)WsqUVhg2%MY9v0w$wab9>vA* z&$gh5M^rml`T5h?Uwqk(bDuo@{^S&^{kNZLd+(j-%0Jv@*uK2k_G@S3P*3;qb)vRd!SWDQ>HnNi2w9BmMjQm0& zXLT0GJ6D(PT)p0Ll_PiLUlxhvNdjCiC#{{vnl<@8gY485-3!x?%nxxbvFx6F{=ELg z47LptPsHNaWN+hNf2TM?c|v1=rM$i5k$TSPrsMTBJxM-gJLeoWO*P%)cXMWag#>eu zzn#ec%?_riKDTN%rTz%mbGm`uNizJ0X6zC7lgo9Y<#hHr-?ej`6BlHX@b#F4diqP% zZ>1X~zFqv|@!;-~$RpnKFD}Y?dn{>F=KN*dYbG52BzW5AcUD&IoSQ!`9?(i=GwGYp zx36z)J%7WikI5P*#VXdwDx5m$%Hh2C?VBV4?xSCx7Ame0&%ASqFDEHt1}neFgZOJd zw=~7Sl8LMmqTn7$MJ1SgrY~~!M zvlk>|KG(*@tPxfb65dzg==Q@DYC)4#|5EVy1rCU=Dc->$qZaEsS>eL;x%HZV4tSh@9NV*7 z#joS9jNhWfb^Z$WV$k&Q_*GX}csy9pJo`kR)4mOF_`iOe zVHi>tz|VgF(g$+^c|n_%{#Tsj;*-xYZh5D6cydFQ`;Nu*Ww3*`JP9wy=e6ach`%2xV@s7S%p5*K_aT z`YGoeV;1K+1a6jGERt#>!gIhcKASB>?A&D|=k-k8#(kzwyB~gIf4X{eYU$CP+v*Yw zR=l0vl-#d%qc8B;+i1Qn-m2{$YR(I1yuSHn@5TD~utjgyrmk1Hx#Kvel}k&ybBtKM zB8L=Pw(w8HuR`8UYO^l4IMq0v@vB4Fn+Rw1iBim$7qCssasGDA{-^F24yASLR6bQS zYbi2piQN6TWSP_Yn^&9OS$==_c2z?5s#u0^0rg4dKHZz!ioboxSh95O+?@ZjO#BKM z18*%qy!h*e$yb+~dVC5z8Wq~sQnG5swG>0{n4d{Y{&^PKSe_0t*>>>yDvRyGZxgTe ze>}(c(?H6|VS#E#_Rcf*Q@Y=XPMx`$vHN(t+r^6o55r$?vb*A;#kA@CjFrrnL#E7K z6kXi9SM6$$YyGb2ifWEci}e##o=LTH_td|X5puR)(rR(!Bg2a$*8=6#KjzJe(ZBq1 z`IMlBBfnCaII^c+3_CidZQk1U0EU&i50A`$`ul z=Tu3^m4GdL@;5|enKm!|_UUNhMd$54X6>8rGN-n(u{%zjf4S>$y;zsdjndC$lL}vM zKkc2l`sZ7Dhj|y?wN~3NN>`ll_M|7q7JXE5_;tD*gQM?ty=O)z@?0Uw0mTC~)N0*-cfeS~jRnFumxw`D(iI z0d=7t)q1avsL5<}kjN39xY)9OKJVd~#n*M6`U^y?`)*yoq;xAMNPoN6?dZ+hcvze? zxn&}wsLag-?>;AH(@Z6a~SBwJqgVpiMt^Rs!bo7pV6x*~Ot z`+8r6bt%~rXSazQYwYzg+a|ElMT#xGwbK^T*+0BSGfFsqpEFiZ~e_y z|B$-$-Hhf^#;0y=cWAlz_r#m9jwRoI1xo>=fu}vKAn9(EUqNG)FG86KVMw=!Iy1}KODbo(B4q~So6M6>T5fW=kKS#e6*g! zE#Z=ziq!4;rV9$;qJq~HyQo0zxUH{*-{_VxQ_z<71mJAIjh zrOeW|ZEjlCT?*N_yV5eM^!2rzyZO4#S(hC?PQNbkvDKls?6^tYNc`J$xQ;LcS(KxOVxi$XT?=}4%x>Q zFMO6?aFFTbgvmyoVo9r_Tnx7C{U=h-d`3}p+wquv97%fd3eUtCj;fy$C@U5znmRlA zejpT z)3quzsjPqgr=xD)ca(0*30)nt+>m} z{mpIZFC3pW)W z{drUJ8TaZ^Q4iwb-9?XW^pG@8Pwq zuJwm3_tgiMy^gql$@};0o^3A#AFsFI_Ivomf2nlKGPdRW`S^7EYTm~&2W@w_v%X=4 zTfkq_V;U@7ZL!&ptMpw27TjRH=yl?#rs|{{*QdHJ5sr@QQ<6=P>fQM(<>!aP?bq%5 z9@i)ydR%uxZ?(ln8^PG;&cBsgzc$2nr|CEKKMTugWaoFUZ~MD#gVDk>nKNeiyXhL( z*SWv)|7pAav;P@ZrvFO)HTU1&n8H@_%KBGYo!64+uScdhWJy2a_HueA)F%0Nz0qTj z%L}J;OK_*Vh@C&eFR@#s^uom3Yl2@|Ror;c{oYS=P7S}Q3rFuqZk2x%Pu$x!pZ{&v z%N=*tw}m~OI45=Aixn^HyZ9FRq^0=iU)*|p{mizkOFupr-iq!CS=Id0SbqO+*~Q+i zeMf8etL>WVQ0}=xd{d5Jb)(zehc)NbnO_#y{jU^HXf5x|nZZh11o&TiHv#|8%M^?GI>` zELl)}WAjelz1B@aEGeyhiFN6RSfB1*HAh)!U-=copmh({@7ZXr)~z|0cipx!^?J35 z-=BK#Tb_36T(rn}$J~#8saMvn*)=sZ{NzT_(&VH~uYE6AIPZPjCAH$f)OTzCCZ|hl z-Vg~EPWW@bMCa8D%~@3o%y#JQzF+^R*Sdbjm0LG$)BVn;=|_L{2{x>fyV`$VUC6R& z27BlR_r-dtvyYz0+9q^m(XCHP>z6H`>tNt*@y6wp@3tj-=PgSWo1AAMw(!iBXC=Qn zYor#s_cdBBI<-WNC$w$0ykv9aR>xa>S8KN}V94yWpJghzoT*>n#)9d)g`bE%I?hmk zC){{$Z2he3>pATt_pRTwblJ1=jydzYHVHrR?zB9@{IOYVyY_pDAG{Tva&~*aw4Kj0 zuvu4|#P^Ulf8O(%cIQ=?&IzQ#!6T3Z|vkm2!cOC6s@^ID%*UjgIW-h&M zCFRDLslH)<4&&iz%Vw=gmzn5)(9b60#hIHN$%)KCZEevO^;^=8eo^=0$$a+zXGWm^ z{zt!id3RoVaou5GXV2>R)*jgtKbe*&ifd*2|M|uz`1{}F8~LpNdVK$0tCQfcwBB|5 zqIia`$JxmVS#9oTcjXIa+c<_^Q!*rTbm}xFGS4%h7~KjQy4W91|E0KXsKD^6PUyzZ5Jth}E6vzzzZn~iH_ z9fKQd;>|6M;>$}I?iJ?T5bvxOQuY6FbB9+y|I~RdpPnc`m-1dyw0d`Lv(%c~E}UoG zD=$5&e{=LeifL8jEWr;yT_X-2FxByBxAF0HbuW_JT5)sZl_!4NcT2<^4f3lNy%un| z$|U5c;oVB_vd4PA9zMvh;rmp%{@J=%;R(^Jne*b*9H*XtE4EBo zm**q&a&wFjuTICX~aEFn((qO>Di%O zohkkn)6L`sZr` z_nrmNcLN=1gBkJ?kvu`E|$v*hI4N@>|kTCS%t2dxOa z+Zb28o_ErU<=-BxYcHLVX`r5YV8hb~TP=Tj9hmWWnqRy~OzzFy7H3!U)IT|7R^}JA2^7l46$4|J_$?`NI7-%4mAwE|v2gmUi{#5f8j??z;D@ ze)4Jup|4d+(+;_t{QJ@Ta>~+O`9;q&9@;+O`;=QCP^pz`J~KgEX2ZG5XXNv5-@Izd z)1xIcm#5z_x$wJGMz_qgsr3_YH$3e8Qt$8a6~doh?%KcAfBMtPvhKb;FO8d| zK3qPZR_VGjf~S0Y?A-F$sy&Gpcb(6Rl*^j?#kABi>5hkLZOxQh#a35$?2**!t$BAN z^veC(D7VO{G@F?D>$@brK7FEMu;-$TW&72R;+Hy~UAN1nTl{F#@k-COL*5)-Zj9?&mOaTim-08n?em$#9UeEPSX2~!wmAAxW6I(Q zJH^gA$Q-vaIp91?>}=}PbDusJ{jhLp*e}f^;_hhqW`BJTf6me^hSkr$#NPW{QlM7R zAGo?t^|ATUX*G5(r%vqkEuFfc*;PV5;SyR@aKKGkuVARorv6LHyvP#6|AX zldrX}ojJ8E&WCA|u(0x_RF;;h=NIjMac)WOB)!AdmNBa~-kfatvi6O7T%G?r#T!+t z!;DgY3!FFq>^yaTV!eo7Wxd>|i_c{fj%B^C)P8?9IpPpk`rNz!k_!)5wLa3wfAb|x zB+;V$*76%?H4c?O^T|3ap>ZMf)xNdM7u?BUcG+>PHN$1*bRz+a%H&fHDk-yf*00|A zTex|zJ?F>o9{{a$Nh3NUnEsg*do99?JD*U^=F<(>dHw; z&K3Hy=j~$q*2;w0)3!v-UiqhNNs`cR|7k3Y`A^CWw!|EbEsiYvzhRG0)sOws(>Je+ zl|HmhRisDpilS;kc{Ee~B91?|+xZ%QH;UX|%hKxm$o0qFj~_R7EN^!C{s zp2S&Fzo&;Sb>Gj&aWVVr*S>z~rL`Me*uN)j{<`t6m72!@zColXy*XV_uj$LeRe^zV4=VcB&y0!PC zt1j*CNtir;zn_!w$6K$am`r;1O8I@%Yk9dV9wpgLJT2d&R-B!cxb%(Y%SG3I-#>L! z`)A$iQ@=7YLjRs&nx1qpYVzZ-cK1B1_-#KGq*tCl7T7wi;gil`-CMnrXYke=rCf^_ zJG1e|vCV(p{kWCnD(9`RZE@k7-pv=R5Zro8?EER~|n1S5Lh< z*{91-%k0j{i$<&#ANnsp;_1~@+njT1Vp*d1$t`9Ve@@amWd9_(`S$%4t!e#_mZ#eu z?Qy&D{`_TQGfu;tJMXlGC4GXY+}$yM-p%^{jY(k*w^aPSdY0&))#VLOw)(kJ{Xp@Y zlv`@XF`BOW_jsp@9d%vv*YU~Mo);d~CKF_{+RxVbK9X$X-K*1Hkt4ihO@Nukt+3y2 z;ihrl?k?;utmY_sz*#(};pOV!jcJ^FtES9}ovFr_)3EuKXsuhc6W2zCB@F9dnZ){R z{(1UMy)wt!;4c%e-#ykQy68=>%#<@rHysjN{fb@8%gT{m^iQYli9I1ZP8{nEU+ z`_i*CuFj2C*in4I_u=eU`~FD((ui5Vz-y;q7^C^2ti{sxiA(>oude#<`0Hc7AMfGx z4V&h$u5OsVK&hpBW2@{Arxu-O*%NII#gmI>wuF0L5sGIlo&GrPg>#jrbPUVB=p)XD zl-zm`2nfxM5bD|~AJ=8Halb^S9al>5LxG1njI%%A`uBk)BguN!$SYT-dg|i>(e*)=OpSL&)xRe+-%y{s@^@iA)BV>R>*s9vC6Aorao!= zV|U-~lZ`57YoD*v`D(K&E7DG<_tJu%)ppr2b}NK3CmcK8RcLndc2vUa8{1yiZn^Dx zT*)kT62ryn`5&G~TrIh>huzmcVzbv=<}kZ=A&l%g8?(KWwX>tRq_#ybuiq;olJ-bs z6W_ukt{$IK8s;S=CmS5LJ+ZO;eum__X^VMh?OnaGw_u{TS5w-gMBNrIsmaq>!xfm9 z>F;EpS-1H~wT9CAjsmGAMXyUf#i@lgtLzQj6&`q?^VEWpo6O5DKd#}|l#MJ{ZhcZJ zNOs0e`Bi!e`EiXq5}x&kTJmgOTspn}vQFZ)zv~A8sX6r9Aiv*@j*Y)c>cgv)7<)(^lm9>}VberbBdUWD$N4&_r<5T`J zvp3B+K3!kdDnxU4^NoOpTeZ_KCWJT$vU!)RUU5k&UgqYW*(Uuy+h_JVd;6@ed);nk zqFDd$#3KKV)<4pD-A%r#oxAr*MNVegB9*<$s(HGR8~egvCjQ;K)A4a(!L~Wy*Dlm} z-gH82l23>va~_2 zcFvB|zVDrX{Y{#sm#CjKVb|A;yVti~{(SnR=9+}0X|YSfg&iEy#N?KDpOjg`y7|qQ zKRWHYb*a-tr_}LZySbXz=+~no+NMI^R^DO#^NOWq!GZ23gJ6`5EAGuQXxMrtr(ZfiY0~wdAKYEsEo)Z9w-(4UyM2wFrC@tx zxoXX;Ubc%4dpOTFoLpdVm3dlu-P*G%N7J84r&Y?o{i^(Wb(PyKfmeGIMAxO>yrHCE zzt`&HW!u2B`Ig^KYMmBK`j8oTCf{twyQj-Oi-p{-a=H2~jrrF(-s{ozPp5Du?vAx; zU|4nQ>C}(uf8-~JAG@RV>FJ^!JIp5eAG-HC``q0Fm)E+7*=H`U3!19gw`ckVh2NcT z`AjExiuidf{mEApZ`wBTWKhGG#*}&d23PpMoMr3T?CbrI!+Hv%nT^xh7q+j%pKeHnY=ikt3wYw;N zeY(DdRn5+hh21{mdk1(J0^!Ra`TZh&5TsnPc+X+*t zN(nyKw(pBx{B~;nd}pnh*!0u9ADUOlE~>j2Qo1A4G4-fecUIWlyyytsPO&o4T$4PW z&GieniwN;bcho3<%3XQldHw37xK-|D-gi3RlxO|3?7M%8FI0P#PO?#A_$;eb|`0H!oH1;+f*Yuw^qDwgg=cPIujNd}d02-<4$RKb!8~`WkbEpIcV9 zK=Pg}APNc5i#CU(-eCPZY_wLhSM>%h9EIfARWv1ERmKD}EM-JLw zI4&OZVuQ}--G^7Kv5S7ASb2Hx?96%bZuSo8y6Kg5H!VE>?))pW{k+lJ)p2=KT(4}X zns|Tf+U~RM&SskRdt(uJHT5z`T1z$X~&J~6!zwc6K7vELLv;28Qj``(Z(s?CyowpabTCbEk`d1$ zfv(|?XH`Ai_;a`6x{HE6J3k6`e(_Q;UUcK4>10p!{=o7HS7O-O|GhrxCmVTp_w$-f z`J%q_KQb*$;D`}0Yc4#!@fP3W^wq%y?DKj`Rioq)>SbM^YXm=rNr&1y9`jh&dyceIX=)RdZD{t*Jmmj9qoA0@~UG1xIyi>4M zY(2Ld+Y9+kKZSZLyf(^Zsa-#|K5;7_qmFND?Q zt}fuN>}c?x-Dq5PcI&U%AD^A_GH>*omu=4Cm-{$-debe&)Oz)wXBb;g^QXPCmEg5q zx%l~&drT{YSFdI)yS?6TbE|r%D;sq{K4=}Fl zUVkwn=tm-3(!K}Yi*E;uytut4{i}Sg+_(Pbr%z*ToGctTb60FRv$~c=f@A8lcL@u= z^jyEb#beXjLf6RkwNIB6)^F!ExxJu>=gfgaQ$k)jMQ-6a%Q`>qsjAF>wxU^250|ip zSMu;5N$e{5_b`buC%^G-AY0N6-5CWUr2an`rx&aM}`Xm*4O9$-onRlzI3UnS|h6tPsp|d3Q8x_4w%c= zcI~~BQt^1h&Yv$TntYG+n?2qp#O>;F)>@25`Kvg4jl<;N_=q`6LvJk zq=!BQ65K}7PKv89_JvonmG51BlF5(hNADx{KgIh_JX}`+nvC1hzx1o$qc?A4j?eS; zJ8%4ac~1STWlxuv9GbNMpYDFK^qa>+?W|AsNbTy$lCAH}dcx*;YnySghfjp|yyt-) z9nV95Ph;JC@NZ3W)m^P%uGg!qkKKO1t6OeIr?Wic(~Opx!HhZyZ2W5cenMtPzjGYf zx%QFJI-RFK6H8StKcBGM!CwDnZo<{O@toTKS$@eiXE3G0yhdf7u|E2_TSUTN{+aj}5N%{Q#lE_+m*e;BRn4H2l*(o^ML5PweB z|DbDQ-^Ixy^(Usk>gew3uMgilac#({W$HilyWiAZzTmcZ;*>MmiEVraDmynM*GFqc ziQInoWJN;7&oxq0%!$xc(H$mwQRFW&l?^PJNowd>iEm_E5@VZ94^St`${R!&muPBFcA z^XMMG6-Qp3-+YvFX+^v59nIr6A3osFnlQgaeVTjW8ntSJ*PqmFr&ixhwp7RzrX+OPk-Z-;()qtW|iZ#B2}>Mb8vu1#8F6aQ!1 zCI9;D!l$}zwy)L{ZJAWT$=eWYbGx1e9&FR7%Y0*wc=t42{n*q#+x%ZSJYpSeJKm(P4&5k}m$JY5 z`m2vS=?}fmvo9cFb+I`n@eaRkPH}#*9Rfn`0zlcv= zohhqysJGycbkyT#H*3On1)krsc=5z3t@{g_?|n5byL~=A<)+uMO^-a@crPh_Xe{Wq zR%G{uuBB>G5BMeD`-?nXr>oNYIq0n__vD|RchQ$5Q$-brxwv-?f|Bx0U^(0l({ zuBG2D9(u4l`tGT$BMnoWON$@==1SM-(Mf(-*qI)}8NcbvnSNohJ?oyndT=o3+l_#@ z`r|!)7T+&OS(``By}VyNKqg|V?#d%kyKXh)UW>Z<`P)+Q&Ff6XCjM$HFfe0Lkb7RW z*X+vOYhP+qju!czd}#OFx_?@}PC>JV8L!xiZz-p)?yqrlRK8@=+|SHZ_TFt(kj3*y zh6yTX#Gkr6e8+2Drz6iPl)ZiD$M8=dx6Ycj-q)=D>n}ImHlI5Wz8uJII}&y8c#H^w(s;boOJs|rL-9Dy45A8lJYd|y=P67(Oq!u z$q)82HqnP7t0&bf+d8tU+y1rY^6m1E)48lUi+z*+JDu3tAikZCg3H&|-aEOu?NfVC z&lI2jLw_^sEv^RNKYOV-*UhK?#|k$iwHJlC%YU&fEe)Q)+00?FYq`#CyXpn|S`R(_ zHL)i*{(J&UMC!2*kvFujNXq6o`u=+r{ltV<#<^gE@7_hmqAa1e>iHAyi0^)~S-vr( zUEr?6q7utjx)Z18J1@9g^3hUvy{p{bm#Z{RUp9$<8TyP%Fm{f>)i3OwF(vg%re=SS zcAvVs-EAZ5-rj^=Uv4V#-+z>Ir}Ook`=)SL{p{q>CDk#bt&+Y<)92bewT zeg!hz=;Y)JiRiM>cU!$qN?`)eysdMh%a$z{VVb@#k72q%V()^emx6P4do?cS?5<<5#%{x6^Jx7F6@k?Dxtl#+kO^?SG7 z+qAzg?=LixiE?AS)+)5()a)3K{G*5BtS%P!Tl%4L(rz#Z^}-ZC8}&KzY_nv%5(|)yb$3Z>EAVea{RZki7K|sStV%ny(wwt ztB95phm>C4kW#I8q&sq<`%v{@fkt~$7K_2$>Ur<^Vn@`)SAaLmlR_s3!Pyp66? zOH-Ge=v^Tj@Khixm{s%N_U;1~Uw`#8i?aKQRvrnM8oT;f-r*~!d4u!IndfX){IWi$ z{Qty~TiglHIgN#iTD^WvSzNhjI`1<^yLy9%Dsk`L^t>uvmT`L**Ne;}UlwKEH&-e= zDslY&%c{28oq2bwb@W^|aQB>^uhwCE@g*Y8eJ{(}#bkB9;ti-zG z4~$}J>esPH)nzkpah`CcSJSHF>W|LS7q4c06EM`sE8HM`R3OcN`{k_$*1M~d|0sNA zyPUg5xLYslRm5q5b(;^y{J&@W^6`mZ0h;UC6}RRt;B7nV?h%^5YW+{~Zmap(tEINw z-fQgsZFeD?L4>sI`ZFP(-K#C!l5*|7aIJXgc28GDS~{qHu4t85%I-kEf;}%&Vg*z# zO4#1c4>7NA$lPmd_pW8_vfYQvc4ckkPI9@lWUk`=d2bAo^AEquOZ&Upan1ESk9BN( z-+w)4zOg{RE#k|0hTY*Nd!_}LJd$RbsM>X*y}h!YRp}e&pYI<#?iz{Y^F;|*igFT2L>kmwz|$_#WR=9_J1AoH_N#Z}Z}UIVP5?idGy~F<2I0Z=vwW z^-^QdGHJ=!yCu#uRreG_TEG!v8L(MoBRhV-pyY#ns^zF zY`^bw=E-posZ42&@>`yvs`#jE-S7F`KYvC(iw^J4)BHdAz?r%J&u%`?sh4}XFywfD zKuDO>|GLOI>zBn7h7A9W*W?<_aH)pz@SY6Q5X-E5KO#S0^vt-*hrm)L}%Zdzi#o|(Yh1GWN+Bh%l z-~MZTCoJks@=A_ZI;SqId_4V`jRf+Obgk|bJJkY6i>6S*#Rq>*j0-@ ze%>t2@ayv@wS<3ra#%h+7f_lQVE(p#w%x6ZNBj1Ec)vO8wyC)lw~%1uhMT*^3Xha# z9OhcG^-Jse!{uxH|2$MwKC+|N(yioKd1Zg;xwhVg;w<(0y?YkB%}%<}VP2-PKY!11 z=Sz1dJTpHwp>E-H{tw4qW=%S`=Rol{<7eNJR|`G+vsl;0KT*<}MGx4!mas|H zyj1TlUEg>Fn7yOSf%O z=Dl_`THW5J-oEnhtEUG)pZv{#-hNM&y-m+_pZbEkagQ#YJ+b!3WhVdoi;r>4_cC=C zvFi)0TVOH!iboVH*C)Bvg?F_0-uzkj^iZbD+$V2(L{506OfA-&A`}+YSTA_?_hgoy zvZ=C9jr7$QJ(H{xxgP7ZSD0_|sUzi+KOARfShKnM{JOKIn{Kv5^3G99-*ElO?Ty>j zcT9HQ{(|NCvAs5X1NE=@2^CirEuMEp?{VC|+KR<-73^FNa&~(wSd<6>R-c=Xph)1h<{4&*F>TY27QlVB5NH6R+!r=-rI>ygN5{>APbM zi=XL=^2%S^Jfy$;xnHjD=Ino9d()O%Hh23t6qL4x#qX0yd)>IY zc86r!wcxW%;V*Wr$?f|5?srUAy8D|rpPoY{ALk!huDFI_W5SUuU0jLQR|}SJnH8yi z{n^g?z2cLWUq8@sd_!XuvuMZ^7w3z@9uL<{J^#z)cKEO73x9CGaQLbonUb8oae}UE ztl#mhFNyl;tJf_!*%Izo+HAAUH@fI;iNl5?dmWyfkoDI7^OSq@++_kwjvZn+YA=1@ z|C_C=bYtG0G+OEV_v}N>mFwhZ2{i{Pt@Yi(bZkk=EaS!XS*xNq=LYO)qZpZdE zGVYjnT+M2!ifoL*f$;jsCo4W3xVY=n3M0WAe{M)I@VXx=*>EqqrM7)#|61?!8TTJQ z-I;t`MJ;>v-h*{t8CdF1KTHVOed7sBZLVDK;i6o33tpG*#&i2?cEb%8~>elOW7|h@38xRS^2)7@!9!R(hlr_Q}f)e_b$D@Vv5uY>B!E1 zduEltn5b1`dTc|S0cX~KDUFrU>@f2_|=lhMO1brTn?0GlM&3y4_nw{`zTi zMSp)}{`35x>o>uKxBqJ^h4{5Ke5U_oW|Qk%^ypL8p}2PbN58FHPW`ReuVuGz`t#b; ze;DWQ^s?Cb;r4_%)7wlYvaA36_#!zf&0kLSJ=pye^k7mvNO&&mmS1bjH~{qnO8-HQjA1S-x&_uFpHUsyLu;)Yt>HEWCg3}5rR zb_GN}<65`lQ%d93{WUuU)o$sgS^0;j3ondg)l6k9`nCSss@)GyUMN^Mt?Pm&+k$Iq z$xDik8<-m#)k`o7Jlq~odw$bN_k?`Ccc~N^l^qL^P*_0V+vO9 zH%$(o?wzpT=*;~`HFn3fFHP6al&{b`G5Kz(b?7R&d)KZe{A@XVNoCcd&g+JKn)=SW z#FuY6@YbTu^hm=5rS81}6AskhJn(eJ^h0{-NlceEdrg9)xn=w zIW|Q08}EuPI=^g(4cqR5$B%M!d2-*~pE@Ciarr}q|CNoCKlgpw^Jk;LNtW+BPKg~l z*xx93mam?tq+I)UP0Y*x9m;WeKCMTdt*zXb;9>suv;1`F4~#+e-NN0!ivpQnKjpf9 z++d;HB$4)Y$#1W2_vcBDTvhH77-Mno^^8z$cc-gU=Y=1grObC&WK~jW>o)JztM-Kb zWJ#$xchSD;ONrqf*-TX_{%7qvWj@`8Q72BcmHpLU^0T`^OZmphK=<-r9JVaGJHNW# z;IntL-~DcDE305Yfx)F)*7cYBBwy6sI(TS>xNT$OZe`Q0kN+6W&a8egu}98UN%+yh zjHgmghm*uU|BRZSucokNU6HB&r&n9r&#PuIy1Wc@5KHk^UDvdd@52Y9YtOV_vphDk z67-kf)}z7umdSCcas-Fml-|H*!DWR#Y})rfZQ1doQS(V_kCM}d)(-VeT%slQi)Di4h z{+P{;YB z9whrw+d*AST43tms<)axvkZM>{#<@kWB2>sj}5vSdb|txfGWC{HzQ z>}YH}HkV;pZ@FAhs%O$lX%FW$uV3wbd@*KkV~hpYiSXL99lvZ2t+rfxdqV#5n2Z@q zShjg<)aY!=7kl*3Or|ul!#(e9^>?HAi0Yr;zJ6{tf3IF|`{TpmCueV;gp1$o9qrKSa8{ZnY?-lM8Ox|{-ar5oF0!q9ej9900e`YkR-^~AFx_j_c z{oa@IAOD+o-T2@6B;$N~)8kmF)+wzg_?BZF3Ii)q7GZa#{J8ZP$F)vMs1q`FL%~ zJhMZgZzamYb*zF__T4UD^DA<*#IgOEcE?|qBBxeL?3Jw3hX;~o~3Hy)d+y%xWuFZG2LDf8 zlhDw-`$}R_-pr1d>T8M_;zj?Pojhx$B6BNwwO&_2!@4`>Jd1PoS)DYIQ;|0OAbK;f zyM1!D|sg3iy*yec> zX4PH)*Sp<|?Z1=xwKL|4!S$`i88Y=d7XI%riW5Bf^6+hyBYDOyY1Otpw^>wHIknGN zIKxC_jr_SVlPmkDX)bxN=$$g>b+n&oO8nF7g`2{ zCe+IYbc_6W*(#aU`tw!x4ewG$ZJh@_984L!OiqthEmvl{uzU7~Ussp5w|~kv+~BCi zp#SCQy!bK|ZY5_<)v!sK-AcAyCnTe9X>si24A`7_g7b9h%MRviYvV3u@A*FO*Az_& zc?*NwiSD1&TQ@e9uiEoHk=ZxA`NMr#VkqO@a1t)fD6{On$y zzf&!DCuwQLX6>EPe{wb!+)VZBQi(WFw)y8qi_UxV!g((nG<)sPQaotdK+4SV~$9vu- zr=2`4e>Kdxy?*tKs^p3pX?#7A606SKW4y6`gS?QQ*P#-Qby-ck+xqFxS*P+|n_{GA66l5>VXOjANQEtNQw&R9+*4y`1 zUJ|VD>RW#8Z9q-HrcG1Nx2uY7ca&VU=jXJY3>N+z_dhK=xt^=UA-RHaN5Ut~y|JrG z_s73e<#Y-Z{r^i&Z$^es;vAX!QmOB~lbx33$%!2M8gs1YK<3Uh^|z&~{u-R;Y=6z> zeVFr@#@vm$3P-<*dvc%a6?*(L(B1OE)(^~g55+R-adL9-%XdGz{2@{4je6UCrlURl z{Y(D0o_PCDbGO#TJp$WquP&*&WoUZfh3z>Z{%1zVBg;0d*nQ0Ad4avjp_O4Pc8Jxh zEv_xSz5K|Ug1!BlmPqJ3Jr$bu=ic5Vd+U?faT4o4ZAvh$WK{J-H$W0Lj8&eDnOC*l{d+~3H%E=iZ^!A517W3BZj34aSG^&6jw z_!Kj%z5GnKL;02+<@xIJrG`!Cj8waYx6Qe6L9agYOVy3ABWsVI6pStTv@V4G;nta>W|;uV9t&%i&>I*!9>Y+y`QQ`@x~dw zzwA%PZ)=}k;w^aM(W1qyF%q8})w;j19JZ-a%3CojGg@=r$Jsg>PrHlGY8NiZ_4O^( zd|Yty|7)A*Sl2K6AK%ze)-mrN<5}6He=G{&Gj4F_OV^Zq7Crp7-s8BDkE7g^wdb~R zh6uPHNMewCcW8%KegDz?6)z%Lr;Q9@k(_h3v>2~c7c%6T@;-h2o)fN{ zo&|ZytiQbUq3o^GY3jUJIo3KUbV)?)S(fFnHA}T4{bI<1GS1V{lcup-b7lk;?XZsNu@Ab0_n5iuilz zyxcZJLYHAeS&^LlOIh}Dr%6w6BfpI za&M4^Oo3U}#$(a59CM~M_8zZ$JUOvp&Bi@bFWlm3z4-dpISt9hkwWqhoqx^VGNrz= zatr@Y_F9G?uCo2Njf1`~U8<&AA)mc}iT&De#s3`U3U2~-NSme|@ChmL3r%_7xus>w z-5Fs#(#M<&&&2o6lI*HG9N=Id${MucZqx;h^j@2YCs$v8a&gj$JDH&Wch9W-?%dA0 zi==sWUOSY`?rURI@3Z-H(#qf4=I`{>zFfjkAALNhc~+j^wrgh&|CN4FkGuun@{#PB#UAMYbX;;&w-q+f%ysw^*;t_chdR%Nq zyIplz$f2st{duuG%^K#n{1aQ=Xr6gc{%+BAGe*nl3v6aYHL|7N3wWOzbNFe5NcWPD zDXAY1Yc*T*Te3dA%$~k^kNExTXWt+Hd-+w4oO

\ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz index 6f30190703efd18965a1d592ec522cd803f04d40..d259eb65eaa18c5f6d235138c124bc7eb8809f73 100644 GIT binary patch delta 879 zcmaDL@<4<`zMF%iw$CbJAqRW?hVOH;b4~Nj3}zmbF?$#P|Cnw%@22Ckj@iuL$scq7 zqOZ5q=PI_%egelWCzcm#N}jlNThwi1k^Ibhjd!eYJ-t8YZ;n5B%WP9{3!~lS zNpF;M%JN^c#>{=HpykVRH{kK~{Xwi#S9(1VUgwtYAvyO_RT)>UyFHtM#36~#HS%** zjo0xd_FcAONvyuTEo}F@1h!KLmO5m;@!q9k8eV^RcgL|Kq2-GvTCVt>q3pXM|I-oM zM+Fm@z9r_(;K&v_{O};h)XTYdSg!;!y{Krp5`174w{V{0md;vryFJab z*cQq!|2VyC^@sWobN<&>-goL@49d&dcl?q~;r|7J27x>LSM_Ohi1^J-VViVg!tLdi z{7NQm#R4*3N4P#!v97yl-zwv=S9SODO8yY14?Pb5nWrwean*2&yDHa=t_7a&&qTDu zNARpN?cwlfNr>6gnEyG+;)&V&P0}ycxTW8DJN4X@caDV{zP?y6xBl~@)#?(~4#~S~ z8Xclo-R1|pe{J(W$>%|ZK|QbH8^IG?cLY|6gyd$4N5-leF!20%6Ud{^eZKEQR3<<3 z{h4|nw=c`vbNGGThPRtpj@@{;;3DVyIc+yYO?qIkrD8-8%ZGnWf#T@}1-L z?JN~j_yugjHkUTOE1z*uWx2tgPs}TNe+AoDGR@T3ng3?(-tb3K$$FX})vqOVa9)mE zmMiX}d(ChDO|EH&eZ~KX|5#tp_I-lrm+t|7uYTTlDt#3vx^_c)6!+zbEolcY@Yt5k zO{=V0cQ*F-UH+mUhy7ly+p~^=>!jVofb)M7gN^JZ`oHj>`Oj$hZlZ0v90LOYO!2kh delta 879 zcmaDL@<4<`zMF$%x6IG*g&gel3%<{NonxABW-#NRgxNd!|HpLGd5ezEI<{xt&iB#x zFZy~*eXd|z>?d&Ca$K3T#tG!6%r}Ep>e~{ zDHYkCin{Y>G%+2%Ggq}@dA^>2%jabm8(jY1KXfnUo$u;NBA*$)9N3?B^Fla`0H!*=7I8+!@gl#UV2ZuZYykFTz+B65k7sFt3Ri>^ziFQ!nSV>f*>;l~>!!`QH+%iN zkCH{KvuE}el?U0_n9fe|n6ESaG^btiwE(`g6E(FRcpm<({uL>+Z^1mrEuFP$b~eqk z{w=t_>|=Y^>JRlFX8o_NyzkV*7?hW@@A#!X1^*WW8U*anU)874A>ubTg>BM}3Ad+L z@++CN6${9C9pU;^#k%g|{uUXJy{fyHSMrBAeduxc%{+C%wX239?y6i8T?^dbp9yJ+ zkKkEl>cipDk`S|}G5>Rd#S^pho1`<=xTW8DJN4X@ceaHazP?y6xBl~j)$9`1_Q~68 z8Xclo-R1|pf4%2llFx$@gL+=YH-aa)?g*?B5y{OGkBn8l@SnZrn?_r6Yr1;H+AZyk z?|G*G2o5&C`>@>p`nO1?;5P;5UbK7<{_OD2dr^n@pTBp{=PUb9kvP}Ve!OVniK*2y zuAI3wPx?uVlIjlTEtfAjh9AifW#9fa=jN}poUi>{ZJS(8#@nQSj!n*UZNATY z9nEBOik(Al_03%l->uJNObVV6`;+mC+uxP*_cZuSxnutA(mn0MUc>2AesE_S9BIjn zyS$Biky!Tf^Es@iA1>nl$NeYV;!yPou3yy}>R \ No newline at end of file + */.pika-single{z-index:9999;display:block;position:relative;color:#333;background:#fff;border:1px solid #ccc;border-bottom-color:#bbb;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.pika-single:after,.pika-single:before{content:" ";display:table}.pika-single:after{clear:both}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-lendar{float:left;width:240px;margin:8px}.pika-title{position:relative;text-align:center}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:700;background-color:#fff}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;filter:alpha(opacity=0);opacity:0}.pika-next,.pika-prev{display:block;cursor:pointer;position:relative;outline:0;border:0;padding:0;width:20px;height:30px;text-indent:20px;white-space:nowrap;overflow:hidden;background-color:transparent;background-position:center center;background-repeat:no-repeat;background-size:75% 75%;opacity:.5}.pika-next:hover,.pika-prev:hover{opacity:1}.is-rtl .pika-next,.pika-prev{float:left;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==)}.is-rtl .pika-prev,.pika-next{float:right;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=)}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-collapse:collapse;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%;padding:0}.pika-table th{color:#999;font-size:12px;line-height:25px;font-weight:700;text-align:center}.pika-button{cursor:pointer;display:block;box-sizing:border-box;-moz-box-sizing:border-box;outline:0;border:0;margin:0;width:100%;padding:5px;color:#666;font-size:12px;line-height:15px;text-align:right;background:#f5f5f5}.pika-week{font-size:11px;color:#999}.is-today .pika-button{color:#3af;font-weight:700}.is-selected .pika-button{color:#fff;font-weight:700;background:#3af;box-shadow:inset 0 1px 3px #178fe5;border-radius:3px}.is-inrange .pika-button{background:#D5E9F7}.is-startrange .pika-button{color:#fff;background:#6CB31D;box-shadow:none;border-radius:3px}.is-endrange .pika-button{color:#fff;background:#3af;box-shadow:none;border-radius:3px}.is-disabled .pika-button{pointer-events:none;cursor:default;color:#999;opacity:.3}.pika-button:hover{color:#fff;background:#ff8000;box-shadow:none;border-radius:3px}.pika-table abbr{border-bottom:none;cursor:help}} \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz index 845f8a31b188913b1c055ef643af2e879b3010af..4bc730683e0dc6e895ce50c295f2be694ab7ba01 100644 GIT binary patch delta 920 zcmdmBxxtb{zMF%iw$CbJBS*AE{pKy_?DhDY#rAQ&wuveIz;$^+=8QASk7mz_ecWHU%~N5nM(4@$k9SCqJS`3gCObu}CRgd5COE+v}8Sbu5(fazw>I7rCqf^aFqxg~?)=zxDc?sVNs*GV4V||e{Z`wKkgc(ok zCn%Zx+MUTD@ZR=PLNS+gk2fm@d~XwQD*Z$Y;?GX6w({XZcnx&=h@tch7$GOvIOqo-VQZe12`@@uA@$kFl)4Fbb zSa2YA(v+T?4=kq6X7P@AXxYBbtHWx;JB7KQt`tTt+j!^unilgVJM!7~z1+6=+O7@r zVq-g8U#I43r*`{&d|uQvB~*?%CY$~KRu-R2UX8M{g)7*T8@KxZNSoXsC9W?uhplqn z-39Zy)wcxQQG4zXaW+1^IbAUP_1@NtMbgvW&z^PodQw{%mifudgIRg zxVc{O-ee8wIInJVuf^}#FD!{Z|LQ~Rh0i;0$lQJKtwVeHhb9A$``qH6lI~PFxvaK| zbJ(GoD6-$^(*q50{@zC~K0FFpQN_67*ICi&sq2mRm@kSKoP0rAx&F1x`6)k+p7cz~ z<4gSSeeYDHg|w#5{R!;bJ_V}iYhMoxzc5L3(v>QfDa$n+H+nc{zct~0yg((e^`K#5 zV$5B?t6O;{MbFt_p;BB`QMBTB#QXm}_cASZoA!wEU-NSe@B5KxJeM(h?~Y$y>I(k# zIzPK@UQP4JV{>Zh!*xz8-#uRU mk)xmUW67u8#-&d=mtFOUI+Fh@_w#**Kk|{QgzdQk7#ILDvA3K6 delta 920 zcmdmBxxtb{zMF$XSL1T{MviES`i)!8+3E2$i|yk)Z4*=Sf$Q>u%o%5tAI+W<{kY|h z((5H3j)=bqI4E^<-lv4U%U8%MtgYGjC){ARD);KkGg%L8=KIdJL;vn(>AbDaHlHkc zYGf6vm|nhpn!)bs6s@a&tWGeNJ37@YH;OOWVgAJTo7d!lJ$!VF;s~SHwVuww@tP-9{ z5{rZK74;qOKDo+p<;mwW6F!-yEH-reU0S*|&RH`k%z;~%HECMy&fE<4h{9=?^D7c2 zNhm0CB-BiBmE~Yf;nUYU@>i*DD#OQJ=7ydJZy6tPyzDT!NpW6b44+1qLD^3y^;y$5 zxLsXtwkTys<^r!z^K~jaGHZAiNZUNfJd(~4>G{)a@)Sv>`q_FaM)ES&D|mG@AI7(7 zcvhTb>5Vy;{&VHeE%OWVXKi0mY5v9LQf;SveuQOEa*FQ%PL7wC_4E!d4?PvI)1+v< z^W_ls_vTl2gueE_7Pmv-wok{&xoMV`zQ=DevLFAQHe>Rff|QD>2HhVf1dE5?EuGeN zt6;%_*hy1*Za%P>I-A8i;-O{xx>M~|8{R0){dA!)GHv6X?`vAjm+Z)8+xK$Y;%U1! z%!!TdFnyhxtDf5J_wjmB)09v-=9p~u_^m8Hm%JKfWebSN zyt@nLb*pa)x}*BsA>!=)wB~fdu-AKAFBVBpTR(f&;peplDlO?yl@d?v1V%IcjW#TM{y9@l%r$F@JVtuhmo3FwVG z^W)-r#kk2D(s5qZ=3a~6vtL*e?f>dS=!MTaZ%E&L@U268`G+Qh6Zg2qFD2cnbaGj3 z73Z)+GhW2r=+grYasJ*%FFrg9SW)$V{jZav(^Ju=Su} zVq#3J-_@-=lcMKruuv|pswi0TJL3KSo_m=VyG?sU`LFRghWGu5HJ;0uy?4hiFLed} zdYzxmHm@dmf8J{FF74xjzBZsXFYjLWWiL>=+}mHT- \ No newline at end of file +case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return this.addPointerListenerMove(t,e,i,n);default:throw"Unknown touch event type"}},addPointerListenerStart:function(t,i,n,s){var a="_leaflet_",r=this._pointers,h=function(t){"mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&o.DomEvent.preventDefault(t);for(var e=!1,i=0;i1))&&(this._moved||(o.DomUtil.addClass(e._mapPane,"leaflet-touching"),e.fire("movestart").fire("zoomstart"),this._moved=!0),o.Util.cancelAnimFrame(this._animRequest),this._animRequest=o.Util.requestAnimFrame(this._updateOnMove,this,!0,this._map._container),o.DomEvent.preventDefault(t))}},_updateOnMove:function(){var t=this._map,e=this._getScaleOrigin(),i=t.layerPointToLatLng(e),n=t.getScaleZoom(this._scale);t._animateZoom(i,n,this._startCenter,this._scale,this._delta,!1,!0)},_onTouchEnd:function(){if(!this._moved||!this._zooming)return void(this._zooming=!1);var t=this._map;this._zooming=!1,o.DomUtil.removeClass(t._mapPane,"leaflet-touching"),o.Util.cancelAnimFrame(this._animRequest),o.DomEvent.off(e,"touchmove",this._onTouchMove).off(e,"touchend",this._onTouchEnd);var i=this._getScaleOrigin(),n=t.layerPointToLatLng(i),s=t.getZoom(),a=t.getScaleZoom(this._scale)-s,r=a>0?Math.ceil(a):Math.floor(a),h=t._limitZoom(s+r),l=t.getZoomScale(h)/this._scale;t._animateZoom(n,h,i,l)},_getScaleOrigin:function(){var t=this._centerOffset.subtract(this._delta).divideBy(this._scale);return this._startCenter.add(t)}}),o.Map.addInitHook("addHandler","touchZoom",o.Map.TouchZoom),o.Map.mergeOptions({tap:!0,tapTolerance:15}),o.Map.Tap=o.Handler.extend({addHooks:function(){o.DomEvent.on(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){o.DomEvent.off(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(o.DomEvent.preventDefault(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new o.Point(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.addClass(n,"leaflet-active"),this._holdTimeout=setTimeout(o.bind(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),o.DomEvent.on(e,"touchmove",this._onMove,this).on(e,"touchend",this._onUp,this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),o.DomEvent.off(e,"touchmove",this._onMove,this).off(e,"touchend",this._onUp,this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],n=i.target;n&&n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.removeClass(n,"leaflet-active"),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var e=t.touches[0];this._newPos=new o.Point(e.clientX,e.clientY)},_simulateEvent:function(i,n){var o=e.createEvent("MouseEvents");o._simulated=!0,n.target._simulatedClick=!0,o.initMouseEvent(i,!0,!0,t,1,n.screenX,n.screenY,n.clientX,n.clientY,!1,!1,!1,!1,0,null),n.target.dispatchEvent(o)}}),o.Browser.touch&&!o.Browser.pointer&&o.Map.addInitHook("addHandler","tap",o.Map.Tap),o.Map.mergeOptions({boxZoom:!0}),o.Map.BoxZoom=o.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._moved=!1},addHooks:function(){o.DomEvent.on(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){o.DomEvent.off(this._container,"mousedown",this._onMouseDown),this._moved=!1},moved:function(){return this._moved},_onMouseDown:function(t){return this._moved=!1,!(!t.shiftKey||1!==t.which&&1!==t.button)&&(o.DomUtil.disableTextSelection(),o.DomUtil.disableImageDrag(),this._startLayerPoint=this._map.mouseEventToLayerPoint(t),void o.DomEvent.on(e,"mousemove",this._onMouseMove,this).on(e,"mouseup",this._onMouseUp,this).on(e,"keydown",this._onKeyDown,this))},_onMouseMove:function(t){this._moved||(this._box=o.DomUtil.create("div","leaflet-zoom-box",this._pane),o.DomUtil.setPosition(this._box,this._startLayerPoint),this._container.style.cursor="crosshair",this._map.fire("boxzoomstart"));var e=this._startLayerPoint,i=this._box,n=this._map.mouseEventToLayerPoint(t),s=n.subtract(e),a=new o.Point(Math.min(n.x,e.x),Math.min(n.y,e.y));o.DomUtil.setPosition(i,a),this._moved=!0,i.style.width=Math.max(0,Math.abs(s.x)-4)+"px",i.style.height=Math.max(0,Math.abs(s.y)-4)+"px"},_finish:function(){this._moved&&(this._pane.removeChild(this._box),this._container.style.cursor=""),o.DomUtil.enableTextSelection(),o.DomUtil.enableImageDrag(),o.DomEvent.off(e,"mousemove",this._onMouseMove).off(e,"mouseup",this._onMouseUp).off(e,"keydown",this._onKeyDown)},_onMouseUp:function(t){this._finish();var e=this._map,i=e.mouseEventToLayerPoint(t);if(!this._startLayerPoint.equals(i)){var n=new o.LatLngBounds(e.layerPointToLatLng(this._startLayerPoint),e.layerPointToLatLng(i));e.fitBounds(n),e.fire("boxzoomend",{boxZoomBounds:n})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}}),o.Map.addInitHook("addHandler","boxZoom",o.Map.BoxZoom),o.Map.mergeOptions({keyboard:!0,keyboardPanOffset:80,keyboardZoomOffset:1}),o.Map.Keyboard=o.Handler.extend({keyCodes:{left:[37],right:[39],down:[40],up:[38],zoomIn:[187,107,61,171],zoomOut:[189,109,173]},initialize:function(t){this._map=t,this._setPanOffset(t.options.keyboardPanOffset),this._setZoomOffset(t.options.keyboardZoomOffset)},addHooks:function(){var t=this._map._container;-1===t.tabIndex&&(t.tabIndex="0"),o.DomEvent.on(t,"focus",this._onFocus,this).on(t,"blur",this._onBlur,this).on(t,"mousedown",this._onMouseDown,this),this._map.on("focus",this._addHooks,this).on("blur",this._removeHooks,this)},removeHooks:function(){this._removeHooks();var t=this._map._container;o.DomEvent.off(t,"focus",this._onFocus,this).off(t,"blur",this._onBlur,this).off(t,"mousedown",this._onMouseDown,this),this._map.off("focus",this._addHooks,this).off("blur",this._removeHooks,this)},_onMouseDown:function(){if(!this._focused){var i=e.body,n=e.documentElement,o=i.scrollTop||n.scrollTop,s=i.scrollLeft||n.scrollLeft;this._map._container.focus(),t.scrollTo(s,o)}},_onFocus:function(){this._focused=!0,this._map.fire("focus")},_onBlur:function(){this._focused=!1,this._map.fire("blur")},_setPanOffset:function(t){var e,i,n=this._panKeys={},o=this.keyCodes;for(e=0,i=o.left.length;i>e;e++)n[o.left[e]]=[-1*t,0];for(e=0,i=o.right.length;i>e;e++)n[o.right[e]]=[t,0];for(e=0,i=o.down.length;i>e;e++)n[o.down[e]]=[0,t];for(e=0,i=o.up.length;i>e;e++)n[o.up[e]]=[0,-1*t]},_setZoomOffset:function(t){var e,i,n=this._zoomKeys={},o=this.keyCodes;for(e=0,i=o.zoomIn.length;i>e;e++)n[o.zoomIn[e]]=t;for(e=0,i=o.zoomOut.length;i>e;e++)n[o.zoomOut[e]]=-t},_addHooks:function(){o.DomEvent.on(e,"keydown",this._onKeyDown,this)},_removeHooks:function(){o.DomEvent.off(e,"keydown",this._onKeyDown,this)},_onKeyDown:function(t){var e=t.keyCode,i=this._map;if(e in this._panKeys){if(i._panAnim&&i._panAnim._inProgress)return;i.panBy(this._panKeys[e]),i.options.maxBounds&&i.panInsideBounds(i.options.maxBounds)}else{if(!(e in this._zoomKeys))return;i.setZoom(i.getZoom()+this._zoomKeys[e])}o.DomEvent.stop(t)}}),o.Map.addInitHook("addHandler","keyboard",o.Map.Keyboard),o.Handler.MarkerDrag=o.Handler.extend({initialize:function(t){this._marker=t},addHooks:function(){var t=this._marker._icon;this._draggable||(this._draggable=new o.Draggable(t,t)),this._draggable.on("dragstart",this._onDragStart,this).on("drag",this._onDrag,this).on("dragend",this._onDragEnd,this),this._draggable.enable(),o.DomUtil.addClass(this._marker._icon,"leaflet-marker-draggable")},removeHooks:function(){this._draggable.off("dragstart",this._onDragStart,this).off("drag",this._onDrag,this).off("dragend",this._onDragEnd,this),this._draggable.disable(),o.DomUtil.removeClass(this._marker._icon,"leaflet-marker-draggable")},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(){this._marker.closePopup().fire("movestart").fire("dragstart")},_onDrag:function(){var t=this._marker,e=t._shadow,i=o.DomUtil.getPosition(t._icon),n=t._map.layerPointToLatLng(i);e&&o.DomUtil.setPosition(e,i),t._latlng=n,t.fire("move",{latlng:n}).fire("drag")},_onDragEnd:function(t){this._marker.fire("moveend").fire("dragend",t)}}),o.Control=o.Class.extend({options:{position:"topright"},initialize:function(t){o.setOptions(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),n=t._controlCorners[i];return o.DomUtil.addClass(e,"leaflet-control"),-1!==i.indexOf("bottom")?n.insertBefore(e,n.firstChild):n.appendChild(e),this},removeFrom:function(t){var e=this.getPosition(),i=t._controlCorners[e];return i.removeChild(this._container),this._map=null,this.onRemove&&this.onRemove(t),this},_refocusOnMap:function(){this._map&&this._map.getContainer().focus()}}),o.control=function(t){return new o.Control(t)},o.Map.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.removeFrom(this),this},_initControlPos:function(){function t(t,s){var a=i+t+" "+i+s;e[t+s]=o.DomUtil.create("div",a,n)}var e=this._controlCorners={},i="leaflet-",n=this._controlContainer=o.DomUtil.create("div",i+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){this._container.removeChild(this._controlContainer)}}),o.Control.Zoom=o.Control.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"-",zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=o.DomUtil.create("div",e+" leaflet-bar");return this._map=t,this._zoomInButton=this._createButton(this.options.zoomInText,this.options.zoomInTitle,e+"-in",i,this._zoomIn,this),this._zoomOutButton=this._createButton(this.options.zoomOutText,this.options.zoomOutTitle,e+"-out",i,this._zoomOut,this),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},_zoomIn:function(t){this._map.zoomIn(t.shiftKey?3:1)},_zoomOut:function(t){this._map.zoomOut(t.shiftKey?3:1)},_createButton:function(t,e,i,n,s,a){var r=o.DomUtil.create("a",i,n);r.innerHTML=t,r.href="#",r.title=e;var h=o.DomEvent.stopPropagation;return o.DomEvent.on(r,"click",h).on(r,"mousedown",h).on(r,"dblclick",h).on(r,"click",o.DomEvent.preventDefault).on(r,"click",s,a).on(r,"click",this._refocusOnMap,a),r},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";o.DomUtil.removeClass(this._zoomInButton,e),o.DomUtil.removeClass(this._zoomOutButton,e),t._zoom===t.getMinZoom()&&o.DomUtil.addClass(this._zoomOutButton,e),t._zoom===t.getMaxZoom()&&o.DomUtil.addClass(this._zoomInButton,e)}}),o.Map.mergeOptions({zoomControl:!0}),o.Map.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new o.Control.Zoom,this.addControl(this.zoomControl))}),o.control.zoom=function(t){return new o.Control.Zoom(t)},o.Control.Attribution=o.Control.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){o.setOptions(this,t),this._attributions={}},onAdd:function(t){this._container=o.DomUtil.create("div","leaflet-control-attribution"),o.DomEvent.disableClickPropagation(this._container);for(var e in t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return t.on("layeradd",this._onLayerAdd,this).on("layerremove",this._onLayerRemove,this),this._update(),this._container},onRemove:function(t){t.off("layeradd",this._onLayerAdd).off("layerremove",this._onLayerRemove)},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):void 0},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):void 0},_update:function(){if(this._map){var t=[];for(var e in this._attributions)this._attributions[e]&&t.push(e);var i=[];this.options.prefix&&i.push(this.options.prefix),t.length&&i.push(t.join(", ")),this._container.innerHTML=i.join(" | ")}},_onLayerAdd:function(t){t.layer.getAttribution&&this.addAttribution(t.layer.getAttribution())},_onLayerRemove:function(t){t.layer.getAttribution&&this.removeAttribution(t.layer.getAttribution())}}),o.Map.mergeOptions({attributionControl:!0}),o.Map.addInitHook(function(){this.options.attributionControl&&(this.attributionControl=(new o.Control.Attribution).addTo(this))}),o.control.attribution=function(t){return new o.Control.Attribution(t)},o.Control.Scale=o.Control.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0,updateWhenIdle:!1},onAdd:function(t){this._map=t;var e="leaflet-control-scale",i=o.DomUtil.create("div",e),n=this.options;return this._addScales(n,e,i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=o.DomUtil.create("div",e+"-line",i)),t.imperial&&(this._iScale=o.DomUtil.create("div",e+"-line",i))},_update:function(){var t=this._map.getBounds(),e=t.getCenter().lat,i=6378137*Math.PI*Math.cos(e*Math.PI/180),n=i*(t.getNorthEast().lng-t.getSouthWest().lng)/180,o=this._map.getSize(),s=this.options,a=0;o.x>0&&(a=n*(s.maxWidth/o.x)),this._updateScales(s,a)},_updateScales:function(t,e){t.metric&&e&&this._updateMetric(e),t.imperial&&e&&this._updateImperial(e)},_updateMetric:function(t){var e=this._getRoundNum(t);this._mScale.style.width=this._getScaleWidth(e/t)+"px",this._mScale.innerHTML=1e3>e?e+" m":e/1e3+" km"},_updateImperial:function(t){var e,i,n,o=3.2808399*t,s=this._iScale;o>5280?(e=o/5280,i=this._getRoundNum(e),s.style.width=this._getScaleWidth(i/e)+"px",s.innerHTML=i+" mi"):(n=this._getRoundNum(o),s.style.width=this._getScaleWidth(n/o)+"px",s.innerHTML=n+" ft")},_getScaleWidth:function(t){return Math.round(this.options.maxWidth*t)-10},_getRoundNum:function(t){var e=Math.pow(10,(Math.floor(t)+"").length-1),i=t/e;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:1,e*i}}),o.control.scale=function(t){return new o.Control.Scale(t)},o.Control.Layers=o.Control.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0},initialize:function(t,e,i){o.setOptions(this,i),this._layers={},this._lastZIndex=0,this._handlingClick=!1;for(var n in t)this._addLayer(t[n],n);for(n in e)this._addLayer(e[n],n,!0)},onAdd:function(t){return this._initLayout(),this._update(),t.on("layeradd",this._onLayerChange,this).on("layerremove",this._onLayerChange,this),this._container},onRemove:function(t){t.off("layeradd",this._onLayerChange,this).off("layerremove",this._onLayerChange,this)},addBaseLayer:function(t,e){return this._addLayer(t,e),this._update(),this},addOverlay:function(t,e){return this._addLayer(t,e,!0),this._update(),this},removeLayer:function(t){var e=o.stamp(t);return delete this._layers[e],this._update(),this},_initLayout:function(){var t="leaflet-control-layers",e=this._container=o.DomUtil.create("div",t);e.setAttribute("aria-haspopup",!0),o.Browser.touch?o.DomEvent.on(e,"click",o.DomEvent.stopPropagation):o.DomEvent.disableClickPropagation(e).disableScrollPropagation(e);var i=this._form=o.DomUtil.create("form",t+"-list");if(this.options.collapsed){o.Browser.android||o.DomEvent.on(e,"mouseover",this._expand,this).on(e,"mouseout",this._collapse,this);var n=this._layersLink=o.DomUtil.create("a",t+"-toggle",e);n.href="#",n.title="Layers",o.Browser.touch?o.DomEvent.on(n,"click",o.DomEvent.stop).on(n,"click",this._expand,this):o.DomEvent.on(n,"focus",this._expand,this),o.DomEvent.on(i,"click",function(){setTimeout(o.bind(this._onInputClick,this),0)},this),this._map.on("click",this._collapse,this)}else this._expand();this._baseLayersList=o.DomUtil.create("div",t+"-base",i),this._separator=o.DomUtil.create("div",t+"-separator",i),this._overlaysList=o.DomUtil.create("div",t+"-overlays",i),e.appendChild(i)},_addLayer:function(t,e,i){var n=o.stamp(t);this._layers[n]={layer:t,name:e,overlay:i},this.options.autoZIndex&&t.setZIndex&&(this._lastZIndex++,t.setZIndex(this._lastZIndex))},_update:function(){if(this._container){this._baseLayersList.innerHTML="",this._overlaysList.innerHTML="";var t,e,i=!1,n=!1;for(t in this._layers)e=this._layers[t],this._addItem(e),n=n||e.overlay,i=i||!e.overlay;this._separator.style.display=n&&i?"":"none"}},_onLayerChange:function(t){var e=this._layers[o.stamp(t.layer)];if(e){this._handlingClick||this._update();var i=e.overlay?"layeradd"===t.type?"overlayadd":"overlayremove":"layeradd"===t.type?"baselayerchange":null;i&&this._map.fire(i,e)}},_createRadioElement:function(t,i){var n='t;t++)e=n[t],i=this._layers[e.layerId],e.checked&&!this._map.hasLayer(i.layer)?this._map.addLayer(i.layer):!e.checked&&this._map.hasLayer(i.layer)&&this._map.removeLayer(i.layer);this._handlingClick=!1,this._refocusOnMap()},_expand:function(){o.DomUtil.addClass(this._container,"leaflet-control-layers-expanded")},_collapse:function(){this._container.className=this._container.className.replace(" leaflet-control-layers-expanded","")}}),o.control.layers=function(t,e,i){return new o.Control.Layers(t,e,i)},o.PosAnimation=o.Class.extend({includes:o.Mixin.Events,run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._newPos=e,this.fire("start"),t.style[o.DomUtil.TRANSITION]="all "+(i||.25)+"s cubic-bezier(0,0,"+(n||.5)+",1)",o.DomEvent.on(t,o.DomUtil.TRANSITION_END,this._onTransitionEnd,this),o.DomUtil.setPosition(t,e),o.Util.falseFn(t.offsetWidth),this._stepTimer=setInterval(o.bind(this._onStep,this),50)},stop:function(){this._inProgress&&(o.DomUtil.setPosition(this._el,this._getPos()),this._onTransitionEnd(),o.Util.falseFn(this._el.offsetWidth))},_onStep:function(){var t=this._getPos();return t?(this._el._leaflet_pos=t,void this.fire("step")):void this._onTransitionEnd()},_transformRe:/([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/,_getPos:function(){var e,i,n,s=this._el,a=t.getComputedStyle(s);if(o.Browser.any3d){if(n=a[o.DomUtil.TRANSFORM].match(this._transformRe),!n)return;e=parseFloat(n[1]),i=parseFloat(n[2])}else e=parseFloat(a.left),i=parseFloat(a.top);return new o.Point(e,i,!0)},_onTransitionEnd:function(){o.DomEvent.off(this._el,o.DomUtil.TRANSITION_END,this._onTransitionEnd,this),this._inProgress&&(this._inProgress=!1,this._el.style[o.DomUtil.TRANSITION]="",this._el._leaflet_pos=this._newPos,clearInterval(this._stepTimer),this.fire("step").fire("end"))}}),o.Map.include({setView:function(t,e,n){if(e=e===i?this._zoom:this._limitZoom(e),t=this._limitCenter(o.latLng(t),e,this.options.maxBounds),n=n||{},this._panAnim&&this._panAnim.stop(),this._loaded&&!n.reset&&n!==!0){n.animate!==i&&(n.zoom=o.extend({animate:n.animate},n.zoom),n.pan=o.extend({animate:n.animate},n.pan));var s=this._zoom!==e?this._tryAnimatedZoom&&this._tryAnimatedZoom(t,e,n.zoom):this._tryAnimatedPan(t,n.pan);if(s)return clearTimeout(this._sizeTimer),this}return this._resetView(t,e),this},panBy:function(t,e){if(t=o.point(t).round(),e=e||{},!t.x&&!t.y)return this;if(this._panAnim||(this._panAnim=new o.PosAnimation,this._panAnim.on({step:this._onPanTransitionStep,end:this._onPanTransitionEnd},this)),e.noMoveStart||this.fire("movestart"),e.animate!==!1){o.DomUtil.addClass(this._mapPane,"leaflet-pan-anim");var i=this._getMapPanePos().subtract(t);this._panAnim.run(this._mapPane,i,e.duration||.25,e.easeLinearity)}else this._rawPanBy(t),this.fire("move").fire("moveend");return this},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){o.DomUtil.removeClass(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,e){var i=this._getCenterOffset(t)._floor();return!((e&&e.animate)!==!0&&!this.getSize().contains(i))&&(this.panBy(i,e),!0)}}),o.PosAnimation=o.DomUtil.TRANSITION?o.PosAnimation:o.PosAnimation.extend({run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=i||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=o.DomUtil.getPosition(t),this._offset=e.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(),this._complete())},_animate:function(){this._animId=o.Util.requestAnimFrame(this._animate,this),this._step()},_step:function(){var t=+new Date-this._startTime,e=1e3*this._duration;e>t?this._runFrame(this._easeOut(t/e)):(this._runFrame(1),this._complete())},_runFrame:function(t){var e=this._startPos.add(this._offset.multiplyBy(t));o.DomUtil.setPosition(this._el,e),this.fire("step")},_complete:function(){o.Util.cancelAnimFrame(this._animId),this._inProgress=!1,this.fire("end")},_easeOut:function(t){return 1-Math.pow(1-t,this._easeOutPower)}}),o.Map.mergeOptions({zoomAnimation:!0,zoomAnimationThreshold:4}),o.DomUtil.TRANSITION&&o.Map.addInitHook(function(){this._zoomAnimated=this.options.zoomAnimation&&o.DomUtil.TRANSITION&&o.Browser.any3d&&!o.Browser.android23&&!o.Browser.mobileOpera,this._zoomAnimated&&o.DomEvent.on(this._mapPane,o.DomUtil.TRANSITION_END,this._catchTransitionEnd,this)}),o.Map.include(o.DomUtil.TRANSITION?{_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,e,i){if(this._animatingZoom)return!0;if(i=i||{},!this._zoomAnimated||i.animate===!1||this._nothingToAnimate()||Math.abs(e-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),o=this._getCenterOffset(t)._divideBy(1-1/n),s=this._getCenterLayerPoint()._add(o);return!(i.animate!==!0&&!this.getSize().contains(o))&&(this.fire("movestart").fire("zoomstart"),this._animateZoom(t,e,s,n,null,!0),!0)},_animateZoom:function(t,e,i,n,s,a,r){r||(this._animatingZoom=!0),o.DomUtil.addClass(this._mapPane,"leaflet-zoom-anim"),this._animateToCenter=t,this._animateToZoom=e,o.Draggable&&(o.Draggable._disabled=!0),o.Util.requestAnimFrame(function(){this.fire("zoomanim",{center:t,zoom:e,origin:i,scale:n,delta:s,backwards:a}),setTimeout(o.bind(this._onZoomTransitionEnd,this),250)},this)},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._animatingZoom=!1,o.DomUtil.removeClass(this._mapPane,"leaflet-zoom-anim"),o.Util.requestAnimFrame(function(){this._resetView(this._animateToCenter,this._animateToZoom,!0,!0),o.Draggable&&(o.Draggable._disabled=!1)},this))}}:{}),o.TileLayer.include({_animateZoom:function(t){this._animating||(this._animating=!0,this._prepareBgBuffer());var e=this._bgBuffer,i=o.DomUtil.TRANSFORM,n=t.delta?o.DomUtil.getTranslateString(t.delta):e.style[i],s=o.DomUtil.getScaleString(t.scale,t.origin);e.style[i]=t.backwards?s+" "+n:n+" "+s},_endZoomAnim:function(){var t=this._tileContainer,e=this._bgBuffer;t.style.visibility="",t.parentNode.appendChild(t),o.Util.falseFn(e.offsetWidth);var i=this._map.getZoom();(i>this.options.maxZoom||i.5&&.5>n?(t.style.visibility="hidden",void this._stopLoadingImages(t)):(e.style.visibility="hidden",e.style[o.DomUtil.TRANSFORM]="",this._tileContainer=e,e=this._bgBuffer=t,this._stopLoadingImages(e),void clearTimeout(this._clearBgBufferTimer))},_getLoadedTilesPercentage:function(t){var e,i,n=t.getElementsByTagName("img"),o=0;for(e=0,i=n.length;i>e;e++)n[e].complete&&o++;return o/i},_stopLoadingImages:function(t){var e,i,n,s=Array.prototype.slice.call(t.getElementsByTagName("img"));for(e=0,i=s.length;i>e;e++)n=s[e],n.complete||(n.onload=o.Util.falseFn,n.onerror=o.Util.falseFn,n.src=o.Util.emptyImageUrl,n.parentNode.removeChild(n))}}),o.Map.include({_defaultLocateOptions:{watch:!1,setView:!1,maxZoom:1/0,timeout:1e4,maximumAge:0,enableHighAccuracy:!1},locate:function(t){if(t=this._locateOptions=o.extend(this._defaultLocateOptions,t),!navigator.geolocation)return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var e=o.bind(this._handleGeolocationResponse,this),i=o.bind(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e=t.code,i=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+i+"."})},_handleGeolocationResponse:function(t){var e=t.coords.latitude,i=t.coords.longitude,n=new o.LatLng(e,i),s=180*t.coords.accuracy/40075017,a=s/Math.cos(o.LatLng.DEG_TO_RAD*e),r=o.latLngBounds([e-s,i-a],[e+s,i+a]),h=this._locateOptions;if(h.setView){var l=Math.min(this.getBoundsZoom(r),h.maxZoom);this.setView(n,l)}var u={latlng:n,bounds:r,timestamp:t.timestamp};for(var c in t.coords)"number"==typeof t.coords[c]&&(u[c]=t.coords[c]);this.fire("locationfound",u)}})}(window,document)- \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz index faab27155876979382593dc3bc5197cf26993c96..e153a5290fbbb505b414fb4b8b7b05b3455437f5 100644 GIT binary patch delta 41988 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 z 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 HgoyzF{tcd* diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index e4ba63ad1bc..9690aa3e87b 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","800fbd5abb63ca9203ed4f3918af72af"],["/frontend/panels/dev-event-550bf85345c454274a40d15b2795a002.html","6977c253b5b4da588d50b0aaa50b21f4"],["/frontend/panels/dev-info-ec613406ce7e20d93754233d55625c8a.html","8e28a4c617fd6963b45103d5e5c80617"],["/frontend/panels/dev-service-4a051878b92b002b8b018774ba207769.html","57123d199ea22cbaaddc46c36b18075f"],["/frontend/panels/dev-state-65e5f791cc467561719bf591f1386054.html","78158786a6597ef86c3fd6f4985cde92"],["/frontend/panels/dev-template-7d744ab7f7c08b6d6ad42069989de400.html","8a6ee994b1cdb45b081299b8609915ed"],["/frontend/panels/map-49ab2d6f180f8bdea7cffaa66b8a5d3e.html","6e6c9c74e0b2424b62d4cc55b8e89be3"],["/static/core-5ed5e063d66eb252b5b288738c9c2d16.js","59dabb570c57dd421d5197009bf1d07f"],["/static/frontend-78be2dfedc4e95326cbcd9401fb17b4d.html","3c1878cbbeb44be763c1c8e3b8a1fb5a"],["/static/mdi-46a76f877ac9848899b8ed382427c16f.html","a846c4082dd5cffd88ac72cbe943e691"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.toString().match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a))return e.add(new Request(a,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e.delete(a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);var n="index.html";!t&&n&&(a=addDirectoryIndex(a,n),t=urlsToCacheKeys.has(a));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(a=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var a,n;for(a=0;aHbGV%L$zZoXR%GFmhr(F$U=Gb@0_jOg{Oy{S0VsCsSK0N)%zk1o8 zuX+CZYadj7kmKoluw|`oXf9W*M_#?n{-#qlk5e^w{CU-szvum~j0bO5&CPe;|2gmO zPR?KA@r8#!USBuoK;O@{^Y8i}Psq=kR=~IY-$d1e)3Y}x-)ek!PKi}j;=td$upbXg ztm0S)EN>fk+hNH9K{# zOUu@0zCQJF{<;8<;6K}?t;_3J?>QJ3oIA7jd3V&^IraZP|DAokG1br1S-Wa^?8meX zISx_J7O=&hStc%i?#4&i6Q4pRn((KpNTzW17s;k+^=oimRC`+Ut#DDE)#fV|C$%+h zZ){pE7%{&;Wl7Kk$+Ij^d{jAoMNXE^TVZfY$#a1-oSs%Qa6fp3!YECGv>k zHKz+2U4E}MwPO4016VsJ8%%ak(n%kCrPu@?bNVfdO05*@R#8>- zW1b`O=tZWfu1R=nOW>iyQw&bI$Sv%hn4olIzKNg>m*&||z00Od(m1yF*@49#T<4}v z@>F~5BOrDu#D2la0_Md$xtC1Tg~Bh2_$u$bkm+aA87a2xq#J*#O1*2b(Y&RWtY-sQ z^L*7Uy_YuqVSK=&Gi_<8tEjJ#-il>LDN4bT;hrLCmFJ3BJtguEK9+Qtr{3hzdZ2T{ zM2+*TvnD@Ru|3M_qphrDvS@CXMxaS>lThCb!D$OSIr}TOTs)Aed7tK4mXc=o+!?L@5s*&V`CX zF>X=IPIOIRSQ?_?aMND+8(-S$N#@!0nKC#1KgTR}D(Ic5aye$g6z*jWs)iGHU*ej* z{rIf-o6`l;=H~wp*I6xj%G030IYq{)g{{y>OXrBJ(kDel=|eJ;g&)-KusiZ*GS^Yf zEY{t+2NnnlaCR0de3f%K+H`)wQKt(lPO~~4);HA2U$9eT^Hx(EKczOE$t_1$b{VQ| zQmJQF+W{wg~jmlcmYR!1c5J2f#v%dxF}qgz7J zQI=0HH{ZY7aJ_~xc==wr+^$0}w4{3H7;kS4V&^@>rK1qrT3KWuv}MyVr#BY!;?HRH z1io>(_lZqaN+)r>#WhDA+soY_-)nE)Ayfa&(D+|lJ?qVB=VyygZOXsATki0+)aW^@ zR_Zp(-`{(r%Jk)JJ{|vv+|>yu&+c)0>DWD0;Ev$OHs|koKXS5c zZ^EtDx0ci&JUh>9|EJ3JfpS?gA07t0^ElLZQmpjd-yaW8Eja3>{XG5h+t3>p;%PH%-_3NJd$lh*wwaDe{akW_t(S6UJy_c1Fy*Bb|?vH{YpSVg4k`+9Yn_w!hzA9bn*|J2Q&;FPA*a z;Ya6+>%>!1@0wSg=znR~8oMuFc+(HTn+xg>miK&r`aJsl?2=Cg?{@9`vs5*-b%qt| zw~aUYtp2S_6^QwKsAi2%;I6xTw-_Gpt}oi&`RL2R2Olho@?TwZZr<*@S0joeVDIlO znLm6RQY#+bb2i@b=qc9+iQ@Ckq2jDB(pnC>UAQ)Phx_$IKluJ_-o&%w&wGx!hhoL| z+UKJuj+5N8#miuelW+LX^IYabn}Ok-ADZwyy|oRZLv`F-2M6W z1-q_X`|^+bm+~bkm!2t;XP(>}-5g!9La0*Mc<=kxd2X>xPlI{5b-Zf*%_>{et@F6g zd|8mQb6eSuI2_>53WeSd^d+u`Q=HSgmr_V0c-**!1*Mg0TM zPwl>(Py4@KeDY~m-ae=1rT>mMg|}|o^k$|&^U>e0<2Sh9XkM~<(rppt_e*Eool;oj zzHg)Z1GXrg*jK@c&+6wbubsM6zn(2t)+VNXPJ^zrdfL4!*PEaF1}it)ok_f~GNk$5 zA~CkKb7vpS+RprAn~Qt>!{v))6+TAaiFojZXWCp=ccV#fU%k5ImKeOVfBx0gL81Ta za@fRjcU)eWZ4>+dk`?pp^;O*G*!Si33l{qpU)_nbfOt9EX#JQPk8B#_CP$^gsP%+1X?$?zd`x?yRkzrDe|) zS6g2D!IJ&1^}J!Yd!z~ delta 2288 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 literal 0 HcmV?d00001 From 0ce3703e3081884473bcc6681d550707c97c1f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 27 Nov 2016 09:29:49 +0100 Subject: [PATCH 065/137] Remove fixed throttle for binary_sensor.command_line and sensor.command_line since the scan_interval is configured trough YAML since #1059 (#4586) * Remove fixed throttle for binary_sensor.command_line and sensor.command_line since the scan_interval is configured trough YAML since #1059 * Clean up imports * Add SCAN_INTERVAL=60 to put default scan_inteval back to 60 --- homeassistant/components/binary_sensor/command_line.py | 3 +-- homeassistant/components/sensor/command_line.py | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 6950e0b80c4..72d0a240809 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.command_line/ """ import logging -from datetime import timedelta import voluptuous as vol @@ -23,7 +22,7 @@ DEFAULT_NAME = 'Binary Command Sensor' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +SCAN_INTERVAL = 60 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND): cv.string, diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index b7372edb0dc..e0700e12903 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/sensor.command_line/ """ import logging import subprocess -from datetime import timedelta import voluptuous as vol @@ -15,14 +14,13 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_COMMAND, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Command Sensor' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +SCAN_INTERVAL = 60 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND): cv.string, @@ -96,7 +94,6 @@ class CommandSensorData(object): self.command = command self.value = None - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data with a shell command.""" _LOGGER.info('Running command: %s', self.command) From 34097cda245775fe6f39c1cc0834a9e395f746d7 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Sun, 27 Nov 2016 09:31:00 +0000 Subject: [PATCH 066/137] Allow generic thermostat tolerance to be customisable to determine the temperature difference required to turn switch on. (#4585) --- .../components/climate/generic_thermostat.py | 17 ++++++---- .../climate/test_generic_thermostat.py | 31 ++++++++++++++++++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 1a0b20dc11e..1b3d20d8b59 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['switch', 'sensor'] -TOL_TEMP = 0.3 +DEFAULT_TOLERANCE = 0.3 +DEFAULT_NAME = 'Generic Thermostat' CONF_NAME = 'name' -DEFAULT_NAME = 'Generic Thermostat' CONF_HEATER = 'heater' CONF_SENSOR = 'target_sensor' CONF_MIN_TEMP = 'min_temp' @@ -32,6 +32,7 @@ CONF_MAX_TEMP = 'max_temp' CONF_TARGET_TEMP = 'target_temp' CONF_AC_MODE = 'ac_mode' CONF_MIN_DUR = 'min_cycle_duration' +CONF_TOLERANCE = 'tolerance' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -42,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), }) @@ -56,23 +58,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): target_temp = config.get(CONF_TARGET_TEMP) ac_mode = config.get(CONF_AC_MODE) min_cycle_duration = config.get(CONF_MIN_DUR) + tolerance = config.get(CONF_TOLERANCE) add_devices([GenericThermostat( hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, - target_temp, ac_mode, min_cycle_duration)]) + target_temp, ac_mode, min_cycle_duration, tolerance)]) class GenericThermostat(ClimateDevice): """Representation of a GenericThermostat device.""" def __init__(self, hass, name, heater_entity_id, sensor_entity_id, - min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): + min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, + tolerance): """Initialize the thermostat.""" self.hass = hass self._name = name self.heater_entity_id = heater_entity_id self.ac_mode = ac_mode self.min_cycle_duration = min_cycle_duration + self._tolerance = tolerance self._active = False self._cur_temp = None @@ -193,7 +198,7 @@ class GenericThermostat(ClimateDevice): return if self.ac_mode: - too_hot = self._cur_temp - self._target_temp > TOL_TEMP + too_hot = self._cur_temp - self._target_temp > self._tolerance is_cooling = self._is_device_active if too_hot and not is_cooling: _LOGGER.info('Turning on AC %s', self.heater_entity_id) @@ -202,7 +207,7 @@ class GenericThermostat(ClimateDevice): _LOGGER.info('Turning off AC %s', self.heater_entity_id) switch.turn_off(self.hass, self.heater_entity_id) else: - too_cold = self._target_temp - self._cur_temp > TOL_TEMP + too_cold = self._target_temp - self._cur_temp > self._tolerance is_heating = self._is_device_active if too_cold and not is_heating: diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index d11d925ef41..1730c3e003b 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -25,6 +25,7 @@ ENT_SWITCH = 'switch.test' MIN_TEMP = 3.0 MAX_TEMP = 65.0 TARGET_TEMP = 42.0 +TOLERANCE = 0.5 class TestSetupClimateGenericThermostat(unittest.TestCase): @@ -84,6 +85,7 @@ class TestClimateGenericThermostat(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', + 'tolerance': 2, 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR }}) @@ -113,7 +115,7 @@ class TestClimateGenericThermostat(unittest.TestCase): 'target_sensor': ENT_SENSOR, 'min_temp': MIN_TEMP, 'max_temp': MAX_TEMP, - 'target_temp': TARGET_TEMP + 'target_temp': TARGET_TEMP, }}) state = self.hass.states.get(ENTITY) self.assertEqual(MIN_TEMP, state.attributes.get('min_temp')) @@ -205,6 +207,30 @@ class TestClimateGenericThermostat(unittest.TestCase): self.assertEqual(SERVICE_TURN_OFF, call.service) self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def test_temp_change_heater_on_within_tolerance(self): + """Test if temperature change turn heater on within tolerance.""" + self._setup_switch(False) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(29) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_heater_on_outside_tolerance(self): + """Test if temperature change doesn't turn heater on outside + tolerance. + """ + self._setup_switch(False) + climate.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(25) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): """Setup the test sensor.""" self.hass.states.set(ENT_SENSOR, temp, { @@ -235,6 +261,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', + 'tolerance': 0.3, 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR, 'ac_mode': True @@ -326,6 +353,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', + 'tolerance': 0.3, 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR, 'ac_mode': True, @@ -418,6 +446,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): assert setup_component(self.hass, climate.DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', + 'tolerance': 0.3, 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR, 'min_cycle_duration': datetime.timedelta(minutes=10) From 767f3d58ff467f392f7ce88967e547236ccd6b2d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Nov 2016 12:13:01 -0800 Subject: [PATCH 067/137] Add websocket_api as frontend dependency --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e19e5f6edec..4d9fb8624d8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -16,7 +16,7 @@ from homeassistant.components.http.const import KEY_DEVELOPMENT from .version import FINGERPRINTS DOMAIN = 'frontend' -DEPENDENCIES = ['api'] +DEPENDENCIES = ['api', 'websocket_api'] URL_PANEL_COMPONENT = '/frontend/panels/{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') From ecf285105cf4de4727029062d784f4af6417b5c1 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sun, 27 Nov 2016 21:21:05 +0100 Subject: [PATCH 068/137] Fixed unit_of_measurement functionality for knx sensor (#4594) --- homeassistant/components/sensor/knx.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 1f5c9a76520..3dce95f7688 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -87,18 +87,11 @@ def update_and_define_min_max(config, minimum_default, class KNXSensorBaseClass(): """Sensor Base Class for all KNX Sensors.""" - _unit_of_measurement = None - @property def cache(self): """We don't want to cache any Sensor Value.""" return False - @property - def unit_of_measurement(self): - """Return the defined Unit of Measurement for the KNX Sensor.""" - return self._unit_of_measurement - class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass): """ @@ -122,6 +115,11 @@ class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass): """Return the Value of the KNX Sensor.""" return self._value + @property + def unit_of_measurement(self): + """Return the defined Unit of Measurement for the KNX Sensor.""" + return self._unit_of_measurement + def update(self): """Update KNX sensor.""" from knxip.conversion import knx2_to_float From be91207830037681352c1791c9433490d42f9ef1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Nov 2016 12:21:20 -0800 Subject: [PATCH 069/137] Upgrade HBMQTT (#4599) --- homeassistant/components/mqtt/server.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index cc240e41a30..7910477c808 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -12,7 +12,7 @@ from homeassistant.components.mqtt import PROTOCOL_311 from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.util.async import run_coroutine_threadsafe -REQUIREMENTS = ['hbmqtt==0.7.1'] +REQUIREMENTS = ['hbmqtt==0.8'] DEPENDENCIES = ['http'] diff --git a/requirements_all.txt b/requirements_all.txt index e248829cd4e..837e14cab77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -146,7 +146,7 @@ ha-ffmpeg==0.15 ha-philipsjs==0.0.1 # homeassistant.components.mqtt.server -hbmqtt==0.7.1 +hbmqtt==0.8 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 From e94b4ec006a81cf8abe21df7bede9d0a7bfcfd7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Nov 2016 12:33:02 -0800 Subject: [PATCH 070/137] Tweak services return result (#4600) * Tweak services return result * Lint --- homeassistant/components/websocket_api.py | 4 ++-- tests/components/test_websocket_api.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 357a677e5cc..09f8699f5d1 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -11,7 +11,7 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.components import api, frontend +from homeassistant.components import frontend from homeassistant.core import callback from homeassistant.remote import JSONEncoder from homeassistant.helpers import config_validation as cv @@ -400,7 +400,7 @@ class ActiveConnection: msg = GET_SERVICES_MESSAGE_SCHEMA(msg) self.send_message(result_message(msg['id'], - api.async_services_json(self.hass))) + self.hass.services.async_services())) def handle_get_config(self, msg): """Handle get config command.""" diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 75c33110580..bdad5032a24 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -6,7 +6,7 @@ from async_timeout import timeout import pytest from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi, api, frontend +from homeassistant.components import websocket_api as wapi, frontend from tests.common import mock_http_component_app @@ -249,7 +249,7 @@ def test_get_services(hass, websocket_client): assert msg['id'] == 5 assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] - assert msg['result'] == api.async_services_json(hass) + assert msg['result'] == hass.services.async_services() @asyncio.coroutine From ff4cb23f2ad3a10e43186524f6d477eff1477806 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Sun, 27 Nov 2016 21:49:21 +0000 Subject: [PATCH 071/137] Update nginx docs (#4603) --- script/nginx-hass | 83 +++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/script/nginx-hass b/script/nginx-hass index 9fc1725f043..274fa105e04 100644 --- a/script/nginx-hass +++ b/script/nginx-hass @@ -66,48 +66,55 @@ # port 8123. # ## +http { + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } -server { - # Update this line to be your domain - server_name example.com; + server { + # Update this line to be your domain + server_name example.com; + + # These shouldn't need to be changed + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + return 301 https://$host$request_uri; + } + + server { + # Update this line to be your domain + server_name example.com; + + # Ensure these lines point to your SSL certificate and key + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + # Use these lines instead if you created a self-signed certificate + # ssl_certificate /etc/nginx/ssl/cert.pem; + # ssl_certificate_key /etc/nginx/ssl/key.pem; + + # Ensure this line points to your dhparams file + ssl_dhparam /etc/nginx/ssl/dhparams.pem; - # These shouldn't need to be changed - listen 80 default_server; - listen [::]:80 default_server ipv6only=on; - return 301 https://$host$request_uri; -} + # These shouldn't need to be changed + listen 443 default_server; + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; + ssl on; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + proxy_buffering off; -server { - # Update this line to be your domain - server_name example.com; - - # Ensure these lines point to your SSL certificate and key - ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; - # Use these lines instead if you created a self-signed certificate - # ssl_certificate /etc/nginx/ssl/cert.pem; - # ssl_certificate_key /etc/nginx/ssl/key.pem; - - # Ensure this line points to your dhparams file - ssl_dhparam /etc/nginx/ssl/dhparams.pem; - - - # These shouldn't need to be changed - listen 443 default_server; - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; - ssl on; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - - proxy_buffering off; - - location / { - proxy_pass http://localhost:8123; - proxy_set_header Host $host; - proxy_redirect http:// https://; + location / { + proxy_pass http://localhost:8123; + proxy_set_header Host $host; + proxy_redirect http:// https://; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } } } From 0d734303a42d8cb56a1fe05adb627a50caa80973 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Nov 2016 14:01:12 -0800 Subject: [PATCH 072/137] HTTP: Fix registering views after start (#4604) --- homeassistant/components/http/__init__.py | 11 +++++++ tests/components/http/test_init.py | 40 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0404f4a0df6..dc18dd2481d 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -289,10 +289,21 @@ class HomeAssistantWSGI(object): else: context = None + # Aiohttp freezes apps after start so that no changes can be made. + # However in Home Assistant components can be discovered after boot. + # This will now raise a RunTimeError. + # To work around this we now fake that we are frozen. + # A more appropriate fix would be to create a new app and + # re-register all redirects, views, static paths. + self.app._frozen = True # pylint: disable=protected-access + self._handler = self.app.make_handler() + self.server = yield from self.hass.loop.create_server( self._handler, self.server_host, self.server_port, ssl=context) + self.app._frozen = False # pylint: disable=protected-access + @asyncio.coroutine def stop(self): """Stop the wsgi server.""" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index a1e0532bc14..cd0d4fe1ffa 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" +import asyncio import requests from homeassistant import bootstrap, const @@ -109,3 +110,42 @@ class TestHttp: assert req.headers.get(allow_origin) == HTTP_BASE_URL assert req.headers.get(allow_headers) == \ const.HTTP_HEADER_HA_AUTH.upper() + + +class TestView(http.HomeAssistantView): + + name = 'test' + url = '/hello' + + @asyncio.coroutine + def get(self, request): + """Return a get request.""" + return 'hello' + + +@asyncio.coroutine +def test_registering_view_while_running(hass, test_client): + """Test that we can register a view while the server is running.""" + yield from bootstrap.async_setup_component( + hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: get_test_instance_port(), + } + } + ) + + yield from bootstrap.async_setup_component(hass, 'api') + + yield from hass.async_start() + + yield from hass.async_block_till_done() + + hass.http.register_view(TestView) + + client = yield from test_client(hass.http.app) + + resp = yield from client.get('/hello') + assert resp.status == 200 + + text = yield from resp.text() + assert text == 'hello' From 038b1c1fc67ca33e0e242a27d6dc80ad7305b675 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sun, 27 Nov 2016 17:19:12 -0500 Subject: [PATCH 073/137] precision properties for climate components (#4562) This lets components declare their precision for temperatures. If nothing is declared, we assume 0.1 C and whole integer precision in F. Currently this supports only WHOLE, HALVES, and TENTHS for precision, but adding other precision levels is pretty straight forward. This also uses proliphix as an example of changing the precision for a platform. Closes bug #4350 --- homeassistant/components/climate/__init__.py | 20 +++++++++++++++++-- homeassistant/components/climate/proliphix.py | 12 ++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index d35b142a8c4..80ef97622d5 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -58,6 +58,11 @@ ATTR_OPERATION_LIST = "operation_list" ATTR_SWING_MODE = "swing_mode" ATTR_SWING_LIST = "swing_list" +# The degree of precision for each platform +PRECISION_WHOLE = 1 +PRECISION_HALVES = 0.5 +PRECISION_TENTHS = 0.1 + CONVERTIBLE_ATTRIBUTE = [ ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, @@ -371,6 +376,14 @@ class ClimateDevice(Entity): else: return STATE_UNKNOWN + @property + def precision(self): + """Return the precision of the system.""" + if self.unit_of_measurement == TEMP_CELSIUS: + return PRECISION_TENTHS + else: + return PRECISION_WHOLE + @property def state_attributes(self): """Return the optional state attributes.""" @@ -569,8 +582,11 @@ class ClimateDevice(Entity): value = convert_temperature(temp, self.temperature_unit, self.unit_of_measurement) - if self.unit_of_measurement == TEMP_CELSIUS: + # Round in the units appropriate + if self.precision == PRECISION_HALVES: + return round(value * 2) / 2.0 + elif self.precision == PRECISION_TENTHS: return round(value, 1) else: - # Users of fahrenheit generally expect integer units. + # PRECISION_WHOLE as a fall back return round(value) diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 5b3708db72e..ef6553cc062 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.proliphix/ import voluptuous as vol from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) + PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE, + ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) import homeassistant.helpers.config_validation as cv @@ -60,6 +61,15 @@ class ProliphixThermostat(ClimateDevice): """Return the name of the thermostat.""" return self._name + @property + def precision(self): + """Return the precision of the system. + + Proliphix temperature values are passed back and forth in the + API as tenths of degrees F (i.e. 690 for 69 degrees). + """ + return PRECISION_TENTHS + @property def device_state_attributes(self): """Return the device specific state attributes.""" From 601193b1d216235c03711ad53fe1131c588d5022 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Sun, 27 Nov 2016 23:33:30 +0100 Subject: [PATCH 074/137] Expose isort preferences for tools. (#4481) * Expose isort preferences for tools. * Adhere to pylints sorted imports requirement. * More documentation, set typing in between stdlib and 3rd party. --- homeassistant/util/async.py | 1 + setup.cfg | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/util/async.py b/homeassistant/util/async.py index de34a127748..58aaa4b0338 100644 --- a/homeassistant/util/async.py +++ b/homeassistant/util/async.py @@ -6,6 +6,7 @@ from asyncio import coroutines from asyncio.futures import Future try: + # pylint: disable=ungrouped-imports from asyncio import ensure_future except ImportError: # Python 3.4.3 and earlier has this as async diff --git a/setup.cfg b/setup.cfg index 6d952083a31..f6cc8bd45b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,3 +10,17 @@ exclude = .venv,.git,.tox,docs,www_static,venv,bin,lib,deps,build [pydocstyle] match_dir = ^((?!\.|www_static).)*$ + +[isort] +# https://github.com/timothycrosley/isort +# https://github.com/timothycrosley/isort/wiki/isort-Settings +# splits long import on multiple lines indented by 4 spaces +multi_line_output = 4 +indent = " " +# by default isort don't check module indexes +not_skip = __init__.py +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +# typing is stdlib on py35 but 3rd party on py34, let it hang in between +known_inbetweens = typing +sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER From 84b12ab0078ed95a4bc3de67e088027ebd919d1f Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Sun, 27 Nov 2016 19:18:47 -0500 Subject: [PATCH 075/137] Nest Cam support (#4292) * start nestcam support * start nestcam support * introduce a access_token_cache_file * Bare minimum to get nest thermostat loading * occaisonally the image works * switch to nest-aware interval for testing * Add Nest Aware awareness * remove duplicate error logging line * Fix nest protect support * address baloobot * fix copy pasta * fix more baloobot * last baloobot thing for now? * Use streaming status to determine online or not. online from nest means its on the network * Fix temperature scale for climate * Add support for eco mode * Fix auto mode for nest climate * update update current_operation and set_operation mode to use constant when possible. try to get setting something working * remove stale comment * unused-argument already disabled globally * Add eco to the end, instead of after off * Simplify conditional when the hass mode is the same as the nest one * away_temperature became eco_temperature, and works with eco mode * Update min/max temp based on locked temperature * Forgot to set locked stuff during construction * Cache image instead of throttling (which returns none), respect NestAware subscription * Fix _time_between_snapshots before the first update * WIP pin authorization * Add some more logging * Working configurator, woo. Fix some hound errors * Updated pin workflow * Deprecate more sensors * Don't update during access of name * Don't update during access of name * Add camera brand * Fix up some syntastic errors * Fix ups ome hound errors * Maybe fix some more? * Move snapshot simulator url checking down into python-nest * Rename _ready_to_update_camera_image to _ready_for_snapshot * More fixes * Set the next time a snapshot can be taken when one is taken to simplify logic * Add a FIXME about update not getting called * Call update during constructor, so values get set at least once * Fix up names * Remove todo about eco, since that's pretty nest * thanks hound * Fix temperature being off for farenheight. * Fix some lint errors, which includes using a git version of python-nest with updated code * generate requirements_all.py * fix pylint * Update nestcam before adding * Fix polling of NestCamera * Lint --- homeassistant/components/camera/nest.py | 109 ++++++++++++++++++++++ homeassistant/components/climate/nest.py | 113 ++++++++++------------- homeassistant/components/nest.py | 105 +++++++++++++++++++-- homeassistant/components/sensor/nest.py | 74 ++++++--------- requirements_all.txt | 6 +- 5 files changed, 289 insertions(+), 118 deletions(-) create mode 100644 homeassistant/components/camera/nest.py diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py new file mode 100644 index 00000000000..20003d4b347 --- /dev/null +++ b/homeassistant/components/camera/nest.py @@ -0,0 +1,109 @@ +""" +Support for Nest Cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.nest/ +""" + +import logging +from datetime import timedelta +import requests +from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) +import homeassistant.components.nest as nest +from homeassistant.util.dt import utcnow + + +DEPENDENCIES = ['nest'] +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) + +NEST_BRAND = "Nest" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup a Nest Cam.""" + if discovery_info is None: + return + camera_devices = hass.data[nest.DATA_NEST].camera_devices() + cameras = [NestCamera(structure, device) + for structure, device in camera_devices] + add_devices(cameras, True) + + +class NestCamera(Camera): + """Representation of a Nest Camera.""" + + def __init__(self, structure, device): + """Initialize a Nest Camera.""" + super(NestCamera, self).__init__() + self.structure = structure + self.device = device + + # data attributes + self._location = None + self._name = None + self._is_online = None + self._is_streaming = None + self._is_video_history_enabled = False + # default to non-NestAware subscribed, but will be fixed during update + self._time_between_snapshots = timedelta(seconds=30) + self._last_image = None + self._next_snapshot_at = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def should_poll(self): + """Nest camera should poll periodically.""" + return True + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._is_streaming + + @property + def brand(self): + """Camera Brand.""" + return NEST_BRAND + + # this doesn't seem to be getting called regularly, for some reason + def update(self): + """Cache value from Python-nest.""" + self._location = self.device.where + self._name = self.device.name + self._is_online = self.device.is_online + self._is_streaming = self.device.is_streaming + self._is_video_history_enabled = self.device.is_video_history_enabled + + if self._is_video_history_enabled: + # NestAware allowed 10/min + self._time_between_snapshots = timedelta(seconds=6) + else: + # otherwise, 2/min + self._time_between_snapshots = timedelta(seconds=30) + + def _ready_for_snapshot(self, now): + return (self._next_snapshot_at is None or + now > self._next_snapshot_at) + + def camera_image(self): + """Return a still image response from the camera.""" + now = utcnow() + if self._ready_for_snapshot(now): + url = self.device.snapshot_url + + try: + response = requests.get(url) + except requests.exceptions.RequestException as error: + _LOGGER.error('Error getting camera image: %s', error) + return None + + self._next_snapshot_at = now + self._time_between_snapshots + self._last_image = response.content + + return self._last_image diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 402cc2b2498..01c3b3782b1 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -14,7 +14,8 @@ from homeassistant.components.climate import ( PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE) from homeassistant.const import ( - TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) + TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) DEPENDENCIES = ['nest'] _LOGGER = logging.getLogger(__name__) @@ -24,10 +25,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1)), }) +STATE_ECO = 'eco' +STATE_HEAT_COOL = 'heat-cool' + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest thermostat.""" + _LOGGER.debug("Setting up nest thermostat") + if discovery_info is None: + return + temp_unit = hass.config.units.temperature_unit + add_devices( [NestThermostat(structure, device, temp_unit) for structure, device in hass.data[DATA_NEST].devices()], @@ -58,9 +67,9 @@ class NestThermostat(ClimateDevice): if self.device.can_heat and self.device.can_cool: self._operation_list.append(STATE_AUTO) + self._operation_list.append(STATE_ECO) + # feature of device - self._has_humidifier = self.device.has_humidifier - self._has_dehumidifier = self.device.has_dehumidifier self._has_fan = self.device.has_fan # data attributes @@ -68,41 +77,24 @@ class NestThermostat(ClimateDevice): self._location = None self._name = None self._humidity = None - self._target_humidity = None self._target_temperature = None self._temperature = None + self._temperature_scale = None self._mode = None self._fan = None - self._away_temperature = None + self._eco_temperature = None + self._is_locked = None + self._locked_temperature = None @property def name(self): """Return the name of the nest, if any.""" - if self._location is None: - return self._name - else: - if self._name == '': - return self._location.capitalize() - else: - return self._location.capitalize() + '(' + self._name + ')' + return self._name @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - if self._has_humidifier or self._has_dehumidifier: - # Move these to Thermostat Device and make them global - return { - "humidity": self._humidity, - "target_humidity": self._target_humidity, - } - else: - # No way to control humidity not show setting - return {} + return self._temperature_scale @property def current_temperature(self): @@ -112,21 +104,17 @@ class NestThermostat(ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - if self._mode == 'cool': - return STATE_COOL - elif self._mode == 'heat': - return STATE_HEAT - elif self._mode == 'range': + if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: + return self._mode + elif self._mode == STATE_HEAT_COOL: return STATE_AUTO - elif self._mode == 'off': - return STATE_OFF else: return STATE_UNKNOWN @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != 'range' and not self.is_away_mode_on: + if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on: return self._target_temperature else: return None @@ -134,10 +122,11 @@ class NestThermostat(ClimateDevice): @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" - if self.is_away_mode_on and self._away_temperature[0]: - # away_temperature is always a low, high tuple - return self._away_temperature[0] - if self._mode == 'range': + if (self.is_away_mode_on or self._mode == STATE_ECO) and \ + self._eco_temperature[0]: + # eco_temperature is always a low, high tuple + return self._eco_temperature[0] + if self._mode == STATE_HEAT_COOL: return self._target_temperature[0] else: return None @@ -145,10 +134,11 @@ class NestThermostat(ClimateDevice): @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" - if self.is_away_mode_on and self._away_temperature[1]: - # away_temperature is always a low, high tuple - return self._away_temperature[1] - if self._mode == 'range': + if (self.is_away_mode_on or self._mode == STATE_ECO) and \ + self._eco_temperature[1]: + # eco_temperature is always a low, high tuple + return self._eco_temperature[1] + if self._mode == STATE_HEAT_COOL: return self._target_temperature[1] else: return None @@ -163,8 +153,7 @@ class NestThermostat(ClimateDevice): target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if target_temp_low is not None and target_temp_high is not None: - - if self._mode == 'range': + if self._mode == STATE_HEAT_COOL: temp = (target_temp_low, target_temp_high) else: temp = kwargs.get(ATTR_TEMPERATURE) @@ -173,14 +162,11 @@ class NestThermostat(ClimateDevice): def set_operation_mode(self, operation_mode): """Set operation mode.""" - if operation_mode == STATE_HEAT: - self.device.mode = 'heat' - elif operation_mode == STATE_COOL: - self.device.mode = 'cool' + if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: + device_mode = operation_mode elif operation_mode == STATE_AUTO: - self.device.mode = 'range' - elif operation_mode == STATE_OFF: - self.device.mode = 'off' + device_mode = STATE_HEAT_COOL + self.device.mode = device_mode @property def operation_list(self): @@ -217,30 +203,33 @@ class NestThermostat(ClimateDevice): @property def min_temp(self): """Identify min_temp in Nest API or defaults if not available.""" - temp = self._away_temperature[0] - if temp is None: - return super().min_temp + if self._is_locked: + return self._locked_temperature[0] else: - return temp + return None @property def max_temp(self): """Identify max_temp in Nest API or defaults if not available.""" - temp = self._away_temperature[1] - if temp is None: - return super().max_temp + if self._is_locked: + return self._locked_temperature[1] else: - return temp + return None def update(self): """Cache value from Python-nest.""" self._location = self.device.where self._name = self.device.name self._humidity = self.device.humidity, - self._target_humidity = self.device.target_humidity, self._temperature = self.device.temperature self._mode = self.device.mode self._target_temperature = self.device.target self._fan = self.device.fan - self._away = self.structure.away - self._away_temperature = self.device.away_temperature + self._away = self.structure.away == 'away' + self._eco_temperature = self.device.eco_temperature + self._locked_temperature = self.device.locked_temperature + self._is_locked = self.device.is_locked + if self.device.temperature == 'C': + self._temperature_scale = TEMP_CELSIUS + else: + self._temperature_scale = TEMP_FAHRENHEIT diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 9f766efe693..10310390dfe 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -10,36 +10,109 @@ import socket import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE) +from homeassistant.helpers import discovery +from homeassistant.const import (CONF_STRUCTURE, CONF_FILENAME) +from homeassistant.loader import get_component +_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-nest==2.11.0'] +REQUIREMENTS = [ + 'git+https://github.com/technicalpickles/python-nest.git' + '@nest-cam' + '#python-nest==3.0.0'] DOMAIN = 'nest' DATA_NEST = 'nest' +NEST_CONFIG_FILE = 'nest.conf' +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string) }) }, extra=vol.ALLOW_EXTRA) +def request_configuration(nest, hass, config): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + if 'nest' in _CONFIGURING: + _LOGGER.debug("configurator failed") + configurator.notify_errors( + _CONFIGURING['nest'], "Failed to configure, please try again.") + return + + def nest_configuration_callback(data): + """The actions to do when our configuration callback is called.""" + _LOGGER.debug("configurator callback") + pin = data.get('pin') + setup_nest(hass, nest, config, pin=pin) + + _CONFIGURING['nest'] = configurator.request_config( + hass, "Nest", nest_configuration_callback, + description=('To configure Nest, click Request Authorization below, ' + 'log into your Nest account, ' + 'and then enter the resulting PIN'), + link_name='Request Authorization', + link_url=nest.authorize_url, + submit_caption="Confirm", + fields=[{'id': 'pin', 'name': 'Enter the PIN', 'type': ''}] + ) + + +def setup_nest(hass, nest, config, pin=None): + """Setup Nest Devices.""" + if pin is not None: + _LOGGER.debug("pin acquired, requesting access token") + nest.request_token(pin) + + if nest.access_token is None: + _LOGGER.debug("no access_token, requesting configuration") + request_configuration(nest, hass, config) + return + + if 'nest' in _CONFIGURING: + _LOGGER.debug("configuration done") + configurator = get_component('configurator') + configurator.request_done(_CONFIGURING.pop('nest')) + + _LOGGER.debug("proceeding with setup") + conf = config[DOMAIN] + hass.data[DATA_NEST] = NestDevice(hass, conf, nest) + + _LOGGER.debug("proceeding with discovery") + discovery.load_platform(hass, 'climate', DOMAIN, {}, config) + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'camera', DOMAIN, {}, config) + _LOGGER.debug("setup done") + + return True + + def setup(hass, config): """Setup the Nest thermostat component.""" import nest - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] + if 'nest' in _CONFIGURING: + return - nest = nest.Nest(username, password) - hass.data[DATA_NEST] = NestDevice(hass, conf, nest) + conf = config[DOMAIN] + client_id = conf[CONF_CLIENT_ID] + client_secret = conf[CONF_CLIENT_SECRET] + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + + access_token_cache_file = hass.config.path(filename) + + nest = nest.Nest( + access_token_cache_file=access_token_cache_file, + client_id=client_id, client_secret=client_secret) + setup_nest(hass, nest, config) return True @@ -85,3 +158,17 @@ class NestDevice(object): except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") + + def camera_devices(self): + """Generator returning list of camera devices.""" + try: + for structure in self.nest.structures: + if structure.name in self._structure: + for device in structure.cameradevices: + yield(structure, device) + else: + _LOGGER.info("Ignoring structure %s, not in %s", + structure.name, self._structure) + except socket.error: + _LOGGER.error( + "Connection error logging into the nest web service.") diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 9f8e7396f93..1173fcedd57 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -11,29 +11,35 @@ import voluptuous as vol from homeassistant.components.nest import DATA_NEST, DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.const import ( - TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS + TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PLATFORM, + CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS ) DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', 'operation_mode', - 'last_ip', - 'local_ip', - 'last_connection', - 'battery_level'] + 'last_connection'] -WEATHER_VARS = {'weather_humidity': 'humidity', - 'weather_temperature': 'temperature', - 'weather_condition': 'condition', - 'wind_speed': 'kph', - 'wind_direction': 'direction'} +SENSOR_TYPES_DEPRECATED = ['battery_health', + 'last_ip', + 'local_ip'] -SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V', - 'kph': 'kph', 'temperature': '°C'} +WEATHER_VARS = {} + +DEPRECATED_WEATHER_VARS = {'weather_humidity': 'humidity', + 'weather_temperature': 'temperature', + 'weather_condition': 'condition', + 'wind_speed': 'kph', + 'wind_direction': 'direction'} + +SENSOR_UNITS = {'humidity': '%', + 'temperature': '°C'} PROTECT_VARS = ['co_status', 'smoke_status', - 'battery_level'] + 'battery_health'] + +PROTECT_VARS_DEPRECATED = ['battery_level'] SENSOR_TEMP_TYPES = ['temperature', 'target'] @@ -51,21 +57,22 @@ PLATFORM_SCHEMA = vol.Schema({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest Sensor.""" nest = hass.data[DATA_NEST] + conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_SENSOR_TYPES) all_sensors = [] for structure, device in chain(nest.devices(), nest.protect_devices()): sensors = [NestBasicSensor(structure, device, variable) - for variable in config[CONF_MONITORED_CONDITIONS] + for variable in conf if variable in SENSOR_TYPES and is_thermostat(device)] sensors += [NestTempSensor(structure, device, variable) - for variable in config[CONF_MONITORED_CONDITIONS] + for variable in conf if variable in SENSOR_TEMP_TYPES and is_thermostat(device)] sensors += [NestWeatherSensor(structure, device, WEATHER_VARS[variable]) - for variable in config[CONF_MONITORED_CONDITIONS] + for variable in conf if variable in WEATHER_VARS and is_thermostat(device)] sensors += [NestProtectSensor(structure, device, variable) - for variable in config[CONF_MONITORED_CONDITIONS] + for variable in conf if variable in PROTECT_VARS and is_protect(device)] all_sensors.extend(sensors) @@ -99,16 +106,7 @@ class NestSensor(Entity): @property def name(self): """Return the name of the nest, if any.""" - if self._location is None: - return "{} {}".format(self._name, self.variable) - else: - if self._name == '': - return "{} {}".format(self._location.capitalize(), - self.variable) - else: - return "{}({}){}".format(self._location.capitalize(), - self._name, - self.variable) + return "{} {}".format(self._name, self.variable) class NestBasicSensor(NestSensor): @@ -138,7 +136,10 @@ class NestTempSensor(NestSensor): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return TEMP_CELSIUS + if self.device.temperature_scale == 'C': + return TEMP_CELSIUS + else: + return TEMP_FAHRENHEIT @property def state(self): @@ -191,19 +192,4 @@ class NestProtectSensor(NestSensor): def update(self): """Retrieve latest state.""" - state = getattr(self.device, self.variable) - if self.variable == 'battery_level': - self._state = getattr(self.device, self.variable) - else: - self._state = 'Unknown' - if state == 0: - self._state = 'Ok' - if state == 1 or state == 2: - self._state = 'Warning' - if state == 3: - self._state = 'Emergency' - - @property - def name(self): - """Return the name of the nest, if any.""" - return "{} {}".format(self._location.capitalize(), self.variable) + self._state = getattr(self.device, self.variable).capitalize() diff --git a/requirements_all.txt b/requirements_all.txt index 837e14cab77..dfb56d186a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,6 +124,9 @@ fuzzywuzzy==0.14.0 # homeassistant.components.device_tracker.bluetooth_le_tracker # gattlib==0.20150805 +# homeassistant.components.nest +git+https://github.com/technicalpickles/python-nest.git@nest-cam#python-nest==3.0.0 + # homeassistant.components.notify.gntp gntp==1.0.3 @@ -437,9 +440,6 @@ python-mpd2==0.5.5 # homeassistant.components.switch.mystrom python-mystrom==0.3.6 -# homeassistant.components.nest -python-nest==2.11.0 - # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From cf57db919e450d2898a079fb9beb5467663de256 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 28 Nov 2016 01:26:46 +0100 Subject: [PATCH 076/137] Refactory aiohttp clientsession handling in HA (#4602) * Refactory aiohttp clientsession handling in HA * remove from core / update platforms / rename file --- homeassistant/components/camera/generic.py | 4 +- homeassistant/components/camera/mjpeg.py | 4 +- homeassistant/components/camera/synology.py | 38 ++---- .../components/media_player/__init__.py | 4 +- homeassistant/components/sensor/yr.py | 4 +- homeassistant/components/switch/hook.py | 13 +- homeassistant/core.py | 12 -- homeassistant/helpers/aiohttp_client.py | 119 ++++++++++++++++++ homeassistant/remote.py | 1 - tests/components/media_player/test_demo.py | 3 +- tests/helpers/test_aiohttp_client.py | 81 ++++++++++++ 11 files changed, 229 insertions(+), 54 deletions(-) create mode 100644 homeassistant/helpers/aiohttp_client.py create mode 100644 tests/helpers/test_aiohttp_client.py diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index ec85e6306d4..a73132282bf 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -18,6 +18,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.exceptions import TemplateError from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe @@ -108,8 +109,9 @@ class GenericCamera(Camera): # async else: try: + websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from self.hass.websession.get( + response = yield from websession.get( url, auth=self._auth) self._last_image = yield from response.read() yield from response.release() diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index a2c35410c55..d96ea4ab0a3 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -101,9 +102,10 @@ class MjpegCamera(Camera): return # connect to stream + websession = async_get_clientsession(self.hass) try: with async_timeout.timeout(10, loop=self.hass.loop): - stream = yield from self.hass.websession.get( + stream = yield from websession.get( self._mjpeg_url, auth=self._auth ) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 9292e839b53..1db83ddf762 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -14,12 +14,13 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPGatewayTimeout import async_timeout -from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP) + CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) +from homeassistant.helpers.aiohttp_client import ( + async_get_clientsession, async_create_clientsession) import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe @@ -59,23 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" - if not config.get(CONF_VERIFY_SSL): - connector = aiohttp.TCPConnector(verify_ssl=False) - - @asyncio.coroutine - def _async_close_connector(event): - """Close websession on shutdown.""" - yield from connector.close() - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_close_connector) - else: - connector = hass.websession.connector - - websession_init = aiohttp.ClientSession( - loop=hass.loop, - connector=connector - ) + verify_ssl = config.get(CONF_VERIFY_SSL) + websession_init = async_get_clientsession(hass, verify_ssl) # Determine API to use for authentication syno_api_url = SYNO_API_URL.format( @@ -118,19 +104,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): syno_auth_url ) - websession_init.detach() - # init websession - websession = aiohttp.ClientSession( - loop=hass.loop, connector=connector, cookies={'id': session_id}) - - @callback - def _async_close_websession(event): - """Close websession on shutdown.""" - websession.detach() - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_close_websession) + websession = async_create_clientsession( + hass, verify_ssl, cookies={'id': session_id}) # Use SessionID to get cameras in system syno_camera_url = SYNO_API_URL.format( diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5665699d4f3..c9df431965b 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.const import ( @@ -705,8 +706,9 @@ def _async_fetch_image(hass, url): content, content_type = (None, None) try: + websession = async_get_clientsession(hass) with async_timeout.timeout(10, loop=hass.loop): - response = yield from hass.websession.get(url) + response = yield from websession.get(url) if response.status == 200: content = yield from response.read() content_type = response.headers.get(CONTENT_TYPE_HEADER) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 6429c9dcaad..e3cc5186230 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -18,6 +18,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_utc_time_change) @@ -155,8 +156,9 @@ class YrData(object): if self._nextrun is None or dt_util.utcnow() >= self._nextrun: try: + websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): - resp = yield from self.hass.websession.get(self._url) + resp = yield from websession.get(self._url) if resp.status != 200: try_again('{} returned {}'.format(self._url, resp.status)) return diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 8f24842212d..29fe8372fab 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -13,6 +13,7 @@ import aiohttp from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,10 +32,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up Hook by getting the access token and list of actions.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) + websession = async_get_clientsession(hass) try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - response = yield from hass.websession.post( + response = yield from websession.post( '{}{}'.format(HOOK_ENDPOINT, 'user/login'), data={ 'username': username, @@ -54,7 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - response = yield from hass.websession.get( + response = yield from websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": data['data']['token']}) data = yield from response.json() @@ -79,7 +81,7 @@ class HookSmartHome(SwitchDevice): def __init__(self, hass, token, device_id, device_name): """Initialize the switch.""" - self._hass = hass + self.hass = hass self._token = token self._state = False self._id = device_id @@ -102,8 +104,9 @@ class HookSmartHome(SwitchDevice): """Send the url to the Hook API.""" try: _LOGGER.debug("Sending: %s", url) - with async_timeout.timeout(TIMEOUT, loop=self._hass.loop): - response = yield from self._hass.websession.get( + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + response = yield from websession.get( url, params={"token": self._token}) data = yield from response.json() except (asyncio.TimeoutError, diff --git a/homeassistant/core.py b/homeassistant/core.py index 42ab117eadc..f358903735b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -18,7 +18,6 @@ import threading from types import MappingProxyType from typing import Optional, Any, Callable, List # NOQA -import aiohttp import voluptuous as vol from voluptuous.humanize import humanize_error @@ -121,21 +120,12 @@ class HomeAssistant(object): self.data = {} self.state = CoreState.not_running self.exit_code = None - self._websession = None @property def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state in (CoreState.starting, CoreState.running) - @property - def websession(self): - """Return an aiohttp session to make web requests.""" - if self._websession is None: - self._websession = aiohttp.ClientSession(loop=self.loop) - - return self._websession - def start(self) -> None: """Start home assistant.""" # Register the async start @@ -295,8 +285,6 @@ class HomeAssistant(object): self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) yield from self.async_block_till_done() self.executor.shutdown() - if self._websession is not None: - yield from self._websession.close() self.state = CoreState.not_running self.loop.stop() diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py new file mode 100644 index 00000000000..a1ec8ac85da --- /dev/null +++ b/homeassistant/helpers/aiohttp_client.py @@ -0,0 +1,119 @@ +"""Helper for aiohttp webclient stuff.""" +import asyncio + +import aiohttp + +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + + +DATA_CONNECTOR = 'aiohttp_connector' +DATA_CONNECTOR_NOTVERIFY = 'aiohttp_connector_notverify' +DATA_CLIENTSESSION = 'aiohttp_clientsession' +DATA_CLIENTSESSION_NOTVERIFY = 'aiohttp_clientsession_notverify' + + +@callback +def async_get_clientsession(hass, verify_ssl=True): + """Return default aiohttp ClientSession. + + This method must be run in the event loop. + """ + if verify_ssl: + key = DATA_CLIENTSESSION + else: + key = DATA_CLIENTSESSION_NOTVERIFY + + if key not in hass.data: + connector = _async_get_connector(hass, verify_ssl) + clientsession = aiohttp.ClientSession( + loop=hass.loop, + connector=connector + ) + _async_register_clientsession_shutdown(hass, clientsession) + hass.data[key] = clientsession + + return hass.data[key] + + +@callback +def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, + **kwargs): + """Create a new ClientSession with kwargs, i.e. for cookies. + + If auto_cleanup is False, you need to call detach() after the session + returned is no longer used. Default is True, the session will be + automatically detached on homeassistant_stop. + + This method must be run in the event loop. + """ + connector = _async_get_connector(hass, verify_ssl) + + clientsession = aiohttp.ClientSession( + loop=hass.loop, + connector=connector, + **kwargs + ) + + if auto_cleanup: + _async_register_clientsession_shutdown(hass, clientsession) + + return clientsession + + +@callback +# pylint: disable=invalid-name +def _async_register_clientsession_shutdown(hass, clientsession): + """Register ClientSession close on homeassistant shutdown. + + This method must be run in the event loop. + """ + @callback + def _async_close_websession(event): + """Close websession.""" + clientsession.detach() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_close_websession) + + +@callback +def _async_get_connector(hass, verify_ssl=True): + """Return the connector pool for aiohttp. + + This method must be run in the event loop. + """ + if verify_ssl: + if DATA_CONNECTOR not in hass.data: + connector = aiohttp.TCPConnector(loop=hass.loop) + hass.data[DATA_CONNECTOR] = connector + + _async_register_connector_shutdown(hass, connector) + else: + connector = hass.data[DATA_CONNECTOR] + else: + if DATA_CONNECTOR_NOTVERIFY not in hass.data: + connector = aiohttp.TCPConnector(loop=hass.loop, verify_ssl=False) + hass.data[DATA_CONNECTOR_NOTVERIFY] = connector + + _async_register_connector_shutdown(hass, connector) + else: + connector = hass.data[DATA_CONNECTOR_NOTVERIFY] + + return connector + + +@callback +# pylint: disable=invalid-name +def _async_register_connector_shutdown(hass, connector): + """Register connector pool close on homeassistant shutdown. + + This method must be run in the event loop. + """ + @asyncio.coroutine + def _async_close_connector(event): + """Close websession on shutdown.""" + yield from connector.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_close_connector) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index fa6cb446c67..c9270e2032f 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -138,7 +138,6 @@ class HomeAssistant(ha.HomeAssistant): self.data = {} self.state = ha.CoreState.not_running self.exit_code = None - self._websession = None self.config.api = local_api def start(self): diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index 3539c73b7dd..c9fb3ad6ff8 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -7,6 +7,7 @@ from homeassistant.bootstrap import setup_component from homeassistant.const import HTTP_HEADER_HA_AUTH import homeassistant.components.media_player as mp import homeassistant.components.http as http +from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION import requests @@ -289,7 +290,7 @@ class TestMediaPlayerWeb(unittest.TestCase): def close(self): pass - self.hass._websession = MockWebsession() + self.hass.data[DATA_CLIENTSESSION] = MockWebsession() assert self.hass.states.is_state(entity_id, 'playing') state = self.hass.states.get(entity_id) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py new file mode 100644 index 00000000000..83e1275819b --- /dev/null +++ b/tests/helpers/test_aiohttp_client.py @@ -0,0 +1,81 @@ +"""Test the aiohttp client helper.""" +import unittest + +import aiohttp + +import homeassistant.helpers.aiohttp_client as client +from homeassistant.util.async import run_callback_threadsafe + +from tests.common import get_test_home_assistant + + +class TestHelpersAiohttpClient(unittest.TestCase): + """Test homeassistant.helpers.aiohttp_client module.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_get_clientsession_with_ssl(self): + """Test init clientsession with ssl.""" + run_callback_threadsafe(self.hass.loop, client.async_get_clientsession, + self.hass).result() + + assert isinstance( + self.hass.data[client.DATA_CLIENTSESSION], aiohttp.ClientSession) + assert isinstance( + self.hass.data[client.DATA_CONNECTOR], aiohttp.TCPConnector) + + def test_get_clientsession_without_ssl(self): + """Test init clientsession without ssl.""" + run_callback_threadsafe(self.hass.loop, client.async_get_clientsession, + self.hass, False).result() + + assert isinstance( + self.hass.data[client.DATA_CLIENTSESSION_NOTVERIFY], + aiohttp.ClientSession) + assert isinstance( + self.hass.data[client.DATA_CONNECTOR_NOTVERIFY], + aiohttp.TCPConnector) + + def test_create_clientsession_with_ssl_and_cookies(self): + """Test create clientsession with ssl.""" + def _async_helper(): + return client.async_create_clientsession( + self.hass, + cookies={'bla': True} + ) + + session = run_callback_threadsafe( + self.hass.loop, + _async_helper, + ).result() + + assert isinstance( + session, aiohttp.ClientSession) + assert isinstance( + self.hass.data[client.DATA_CONNECTOR], aiohttp.TCPConnector) + + def test_create_clientsession_without_ssl_and_cookies(self): + """Test create clientsession without ssl.""" + def _async_helper(): + return client.async_create_clientsession( + self.hass, + False, + cookies={'bla': True} + ) + + session = run_callback_threadsafe( + self.hass.loop, + _async_helper, + ).result() + + assert isinstance( + session, aiohttp.ClientSession) + assert isinstance( + self.hass.data[client.DATA_CONNECTOR_NOTVERIFY], + aiohttp.TCPConnector) From f0db698f75e4d0026176a92cd41bd53a4413c214 Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Mon, 28 Nov 2016 02:15:28 +0100 Subject: [PATCH 077/137] Light effects (#4538) * Add support for light effects * Move PLATFORM_SCHEMA changes in light to mqtt_template * Add effect validation * Add unittests * Add light effect to demo and unittests * Use cv.string for config validation * Use cv.ensure_list for config validation * Fix typo * Remove unused exception management for effect --- homeassistant/components/light/__init__.py | 21 +++++++-- homeassistant/components/light/demo.py | 33 +++++++++++--- .../components/light/mqtt_template.py | 44 ++++++++++++++++--- tests/components/light/test_demo.py | 4 +- tests/components/light/test_mqtt_template.py | 42 ++++++++++++++---- 5 files changed, 120 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b4708164fe2..c4ed91af0af 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -64,6 +64,9 @@ ATTR_FLASH = "flash" FLASH_SHORT = "short" FLASH_LONG = "long" +# List of possible effects +ATTR_EFFECT_LIST = "effect_list" + # Apply an effect to the light, can be EFFECT_COLORLOOP. ATTR_EFFECT = "effect" EFFECT_COLORLOOP = "colorloop" @@ -78,6 +81,8 @@ PROP_TO_ATTR = { 'rgb_color': ATTR_RGB_COLOR, 'xy_color': ATTR_XY_COLOR, 'white_value': ATTR_WHITE_VALUE, + 'effect_list': ATTR_EFFECT_LIST, + 'effect': ATTR_EFFECT, 'supported_features': ATTR_SUPPORTED_FEATURES, } @@ -87,10 +92,10 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) LIGHT_TURN_ON_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, - ATTR_PROFILE: str, + ATTR_PROFILE: cv.string, ATTR_TRANSITION: VALID_TRANSITION, ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_COLOR_NAME: str, + ATTR_COLOR_NAME: cv.string, ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)), ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), @@ -99,7 +104,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ max=color_util.HASS_COLOR_MAX)), ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), - ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]), + ATTR_EFFECT: cv.string, }) LIGHT_TURN_OFF_SCHEMA = vol.Schema({ @@ -314,6 +319,16 @@ class Light(ToggleEntity): """Return the white value of this light between 0..255.""" return None + @property + def effect_list(self): + """Return the list of supported effects.""" + return None + + @property + def effect(self): + """Return the current effect.""" + return None + @property def state_attributes(self): """Return optional state attributes.""" diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index e68bde8f379..b6048da243d 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -7,25 +7,29 @@ https://home-assistant.io/components/demo/ import random from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, - ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, + Light) LIGHT_COLORS = [ [237, 224, 33], [255, 63, 111], ] +LIGHT_EFFECT_LIST = ['rainbow', 'none'] + LIGHT_TEMPS = [240, 380] -SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_WHITE_VALUE) +SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | + SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the demo light platform.""" add_devices_callback([ - DemoLight("Bed Light", False), + DemoLight("Bed Light", False, effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0]), DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]), DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0]) ]) @@ -36,7 +40,7 @@ class DemoLight(Light): def __init__( self, name, state, rgb=None, ct=None, brightness=180, - xy_color=(.5, .5), white=200): + xy_color=(.5, .5), white=200, effect_list=None, effect=None): """Initialize the light.""" self._name = name self._state = state @@ -45,6 +49,8 @@ class DemoLight(Light): self._brightness = brightness self._xy_color = xy_color self._white = white + self._effect_list = effect_list + self._effect = effect @property def should_poll(self): @@ -81,6 +87,16 @@ class DemoLight(Light): """Return the white value of this light between 0..255.""" return self._white + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self): + """Return the current effect.""" + return self._effect + @property def is_on(self): """Return true if light is on.""" @@ -110,6 +126,9 @@ class DemoLight(Light): if ATTR_WHITE_VALUE in kwargs: self._white = kwargs[ATTR_WHITE_VALUE] + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT] + self.update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 4566a383645..f632ba37236 100755 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -10,8 +10,8 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA, - ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH, + ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( @@ -27,6 +27,7 @@ DEPENDENCIES = ['mqtt'] DEFAULT_NAME = 'MQTT Template Light' DEFAULT_OPTIMISTIC = False +CONF_EFFECT_LIST = "effect_list" CONF_COMMAND_ON_TEMPLATE = 'command_on_template' CONF_COMMAND_OFF_TEMPLATE = 'command_off_template' CONF_STATE_TEMPLATE = 'state_template' @@ -34,12 +35,14 @@ CONF_BRIGHTNESS_TEMPLATE = 'brightness_template' CONF_RED_TEMPLATE = 'red_template' CONF_GREEN_TEMPLATE = 'green_template' CONF_BLUE_TEMPLATE = 'blue_template' +CONF_EFFECT_TEMPLATE = 'effect_template' -SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | +SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, @@ -49,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template, vol.Optional(CONF_BLUE_TEMPLATE): cv.template, + vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), @@ -61,6 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([MqttTemplate( hass, config.get(CONF_NAME), + config.get(CONF_EFFECT_LIST), { key: config.get(key) for key in ( CONF_STATE_TOPIC, @@ -75,7 +80,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): CONF_BRIGHTNESS_TEMPLATE, CONF_RED_TEMPLATE, CONF_GREEN_TEMPLATE, - CONF_BLUE_TEMPLATE + CONF_BLUE_TEMPLATE, + CONF_EFFECT_TEMPLATE ) }, config.get(CONF_OPTIMISTIC), @@ -87,10 +93,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MqttTemplate(Light): """Representation of a MQTT Template light.""" - def __init__(self, hass, name, topics, templates, optimistic, qos, retain): + def __init__(self, hass, name, effect_list, topics, templates, optimistic, + qos, retain): """Initialize MQTT Template light.""" self._hass = hass self._name = name + self._effect_list = effect_list self._topics = topics self._templates = templates for tpl in self._templates.values(): @@ -114,6 +122,7 @@ class MqttTemplate(Light): self._rgb = [0, 0, 0] else: self._rgb = None + self._effect = None def state_received(topic, payload, qos): """A new MQTT message has been received.""" @@ -152,6 +161,17 @@ class MqttTemplate(Light): except ValueError: _LOGGER.warning('Invalid color value received') + # read effect + if self._templates[CONF_EFFECT_TEMPLATE] is not None: + effect = self._templates[CONF_EFFECT_TEMPLATE].\ + render_with_possible_json_value(payload) + + # validate effect value + if effect in self._effect_list: + self._effect = effect + else: + _LOGGER.warning('Unsupported effect value received') + self.update_ha_state() if self._topics[CONF_STATE_TOPIC] is not None: @@ -191,6 +211,16 @@ class MqttTemplate(Light): """Return True if unable to access real state of the entity.""" return self._optimistic + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self): + """Return the current effect.""" + return self._effect + def turn_on(self, **kwargs): """Turn the entity on.""" # state @@ -214,6 +244,10 @@ class MqttTemplate(Light): if self._optimistic: self._rgb = kwargs[ATTR_RGB_COLOR] + # effect + if ATTR_EFFECT in kwargs: + values['effect'] = kwargs.get(ATTR_EFFECT) + # flash if ATTR_FLASH in kwargs: values['flash'] = kwargs.get(ATTR_FLASH) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 759127c75f9..aa8c8d9f1e8 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -37,6 +37,7 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( (82, 91, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253), white_value=254) @@ -45,10 +46,11 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) self.assertEqual( (251, 252, 253), state.attributes.get(light.ATTR_RGB_COLOR)) - light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400) + light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) + self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT)) def test_turn_off(self): """Test light turn off method.""" diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 94cd2a0a19f..954f3087646 100755 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -90,22 +90,24 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) - def test_state_brightness_color_change_via_topic(self): \ + def test_state_brightness_color_effect_change_via_topic(self): \ # pylint: disable=invalid-name - """Test state, brightness and color change via topic.""" + """Test state, brightness, color and effect change via topic.""" self.hass.config.components = ['mqtt'] with assert_setup_component(1): assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_template', 'name': 'test', + 'effect_list': ['rainbow', 'colorloop'], 'state_topic': 'test_light_rgb', 'command_topic': 'test_light_rgb/set', 'command_on_template': 'on,' '{{ brightness|d }},' '{{ red|d }}-' '{{ green|d }}-' - '{{ blue|d }}', + '{{ blue|d }},' + '{{ effect|d }}', 'command_off_template': 'off', 'state_template': '{{ value.split(",")[0] }}', 'brightness_template': '{{ value.split(",")[1] }}', @@ -114,7 +116,8 @@ class TestLightMQTTTemplate(unittest.TestCase): 'green_template': '{{ value.split(",")[2].' 'split("-")[1] }}', 'blue_template': '{{ value.split(",")[2].' - 'split("-")[2] }}' + 'split("-")[2] }}', + 'effect_template': '{{ value.split(",")[3] }}' } }) @@ -122,16 +125,18 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255') + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255,') self.hass.block_till_done() state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('effect')) # turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', 'off') @@ -155,6 +160,13 @@ class TestLightMQTTTemplate(unittest.TestCase): light_state = self.hass.states.get('light.test') self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + # change the effect + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,41-42-43,rainbow') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual('rainbow', light_state.attributes.get('effect')) + def test_optimistic(self): \ # pylint: disable=invalid-name """Test optimistic mode.""" @@ -314,13 +326,15 @@ class TestLightMQTTTemplate(unittest.TestCase): light.DOMAIN: { 'platform': 'mqtt_template', 'name': 'test', + 'effect_list': ['rainbow', 'colorloop'], 'state_topic': 'test_light_rgb', 'command_topic': 'test_light_rgb/set', 'command_on_template': 'on,' '{{ brightness|d }},' '{{ red|d }}-' '{{ green|d }}-' - '{{ blue|d }}', + '{{ blue|d }},' + '{{ effect|d }}', 'command_off_template': 'off', 'state_template': '{{ value.split(",")[0] }}', 'brightness_template': '{{ value.split(",")[1] }}', @@ -329,7 +343,8 @@ class TestLightMQTTTemplate(unittest.TestCase): 'green_template': '{{ value.split(",")[2].' 'split("-")[1] }}', 'blue_template': '{{ value.split(",")[2].' - 'split("-")[2] }}' + 'split("-")[2] }}', + 'effect_template': '{{ value.split(",")[3] }}', } }) @@ -337,16 +352,19 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) + self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light, full white - fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,255-255-255') + fire_mqtt_message(self.hass, 'test_light_rgb', + 'on,255,255-255-255,rainbow') self.hass.block_till_done() state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual('rainbow', state.attributes.get('effect')) # bad state value fire_mqtt_message(self.hass, 'test_light_rgb', 'offf') @@ -371,3 +389,11 @@ class TestLightMQTTTemplate(unittest.TestCase): # color should not have changed state = self.hass.states.get('light.test') self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + + # bad effect value + fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white') + self.hass.block_till_done() + + # effect should not have changed + state = self.hass.states.get('light.test') + self.assertEqual('rainbow', state.attributes.get('effect')) From d4bc8e23af6ce565b6d81cc8ebff690319883d0b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Nov 2016 17:20:58 -0800 Subject: [PATCH 078/137] Update frontend --- homeassistant/components/frontend/version.py | 6 +++--- .../components/frontend/www_static/core.js | 8 ++++---- .../components/frontend/www_static/core.js.gz | Bin 33404 -> 33322 bytes .../frontend/www_static/frontend.html | 2 +- .../frontend/www_static/frontend.html.gz | Bin 130153 -> 130286 bytes .../www_static/home-assistant-polymer | 2 +- .../panels/ha-panel-dev-service.html | 2 +- .../panels/ha-panel-dev-service.html.gz | Bin 17440 -> 17702 bytes .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 2325 -> 2322 bytes 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 92cf35a7803..fabb6a58da8 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,13 +1,13 @@ """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" FINGERPRINTS = { - "core.js": "525498104891894d97cbf0caf7291edc", - "frontend.html": "18667e347b85a368724308bb1b9485b4", + "core.js": "526d7d704ae478c30ae20c1426c2e4f4", + "frontend.html": "c65df08be08a7329ee01a273af02d6a4", "mdi.html": "46a76f877ac9848899b8ed382427c16f", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e", "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a", - "panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769", + "panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index 6380a6fcaf1..464e4440534 100644 --- a/homeassistant/components/frontend/www_static/core.js +++ b/homeassistant/components/frontend/www_static/core.js @@ -1,4 +1,4 @@ -!(function(){"use strict";function t(t){return t&&t.__esModule?t.default:t}function e(t,e){return e={exports:{}},t(e,e.exports),e.exports}function n(t,e){var n=e.authToken,r=e.host;return Ce({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function r(){return De.getInitialState()}function i(t,e){var n=e.errorMessage;return t.withMutations((function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)}))}function o(t,e){var n=e.authToken,r=e.host;return ze({authToken:n,host:r})}function u(){return Me.getInitialState()}function a(t,e){var n=e.rememberAuth;return n}function s(t){return t.withMutations((function(t){t.set("isStreaming",!0).set("hasError",!1)}))}function c(t){return t.withMutations((function(t){t.set("isStreaming",!1).set("hasError",!0)}))}function f(){return Ue.getInitialState()}function h(t,e,n,r){void 0===r&&(r=null);var i=t.evaluate(yo.authInfo),o=i.host+"/api/"+n;return new Promise(function(t,n){var u=new XMLHttpRequest;u.open(e,o,!0),u.setRequestHeader("X-HA-access",i.authToken),u.onload=function(){var e;try{e="application/json"===u.getResponseHeader("content-type")?JSON.parse(u.responseText):u.responseText}catch(t){e=u.responseText}u.status>199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function l(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?Ye({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||Ye;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function ut(t,e){var n=e.date;return n.toISOString()}function at(){return Pr.getInitialState()}function st(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Hr({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],Hr(e.map(cn.fromJSON)))}))}))}function ct(){return xr.getInitialState()}function ft(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,Gr(e.map(cn.fromJSON)))}))}))}function ht(){return Kr.getInitialState()}function lt(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(Jr,r)}))}function pt(){return Wr.getInitialState()}function _t(t,e){t.dispatch(kr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function dt(t,e){void 0===e&&(e=null),t.dispatch(kr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),Ge(t,"GET",n).then((function(e){return t.dispatch(kr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(kr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function vt(t,e){return t.dispatch(kr.ENTITY_HISTORY_FETCH_START,{date:e}),Ge(t,"GET","history/period/"+e).then((function(n){return t.dispatch(kr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(kr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function yt(t){var e=t.evaluate(Zr);return vt(t,e)}function gt(t){t.registerStores({currentEntityHistoryDate:Pr,entityHistory:xr,isLoadingEntityHistory:qr,recentEntityHistory:Kr,recentEntityHistoryUpdated:Wr})}function mt(t){t.registerStores({moreInfoEntityId:Lr})}function St(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;o0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function kt(t){var e=Wi[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),Wi[t.hassId]=!1)}function Nt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),kt(t);var r=t.evaluate(yo.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",xe(i,{authToken:r}).then((function(e){var r=jt((function(){return e.ping()}),Yi);r(),e.socket.addEventListener("message",r),Wi[t.hassId]={conn:e,scheduleHealthCheck:r},Ji.forEach((function(n){return e.subscribeEvents(Bi.bind(null,t),n)})),t.batch((function(){t.dispatch(ke.STREAM_START),n&&Fi.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(ke.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(ke.STREAM_START),Fi.fetchAll(t)}))}))}))}function Pt(t){t.registerStores({streamStatus:Ue})}function Ut(t,e,n){void 0===n&&(n={});var r=n.rememberAuth;void 0===r&&(r=!1);var i=n.host;void 0===i&&(i=""),t.dispatch(Te.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),Fi.fetchAll(t).then((function(){t.dispatch(Te.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),to.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=ro),t.dispatch(Te.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Ht(t){t.dispatch(Te.LOG_OUT,{})}function xt(t){t.registerStores({authAttempt:De,authCurrent:Me,rememberAuth:je})}function Vt(){if(!("localStorage"in window))return{};var t=window.localStorage,e="___test";try{return t.setItem(e,e),t.removeItem(e),t}catch(t){return{}}}function qt(){var t=new Io({debug:!1});return t.hassId=Oo++,t}function Ft(t,e,n){Object.keys(n).forEach((function(r){var i=n[r];if("register"in i&&i.register(e),"getters"in i&&Object.defineProperty(t,r+"Getters",{value:i.getters,enumerable:!0}),"actions"in i){var o={};Object.getOwnPropertyNames(i.actions).forEach((function(t){"function"==typeof i.actions[t]&&Object.defineProperty(o,t,{value:i.actions[t].bind(null,e),enumerable:!0})})),Object.defineProperty(t,r+"Actions",{value:o,enumerable:!0})}}))}function Gt(t,e){return wo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function Kt(t){return Ge(t,"GET","error_log")}function Bt(t,e){var n=e.date;return n.toISOString()}function Yt(){return Lo.getInitialState()}function Jt(t,e){var n=e.date,r=e.entries;return t.set(n,xo(r.map(Uo.fromJSON)))}function Wt(){return Vo.getInitialState()}function Xt(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function Qt(){return Go.getInitialState()}function Zt(t,e){t.dispatch(zo.LOGBOOK_DATE_SELECTED,{date:e})}function $t(t,e){t.dispatch(zo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),Ge(t,"GET","logbook/"+e).then((function(n){return t.dispatch(zo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(zo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function te(t){return!t||(new Date).getTime()-t>Yo}function ee(t){t.registerStores({currentLogbookDate:Lo,isLoadingLogbookEntries:ko,logbookEntries:Vo,logbookEntriesUpdated:Go})}function ne(t){return t.set("active",!0)}function re(t){return t.set("active",!1)}function ie(){return ou.getInitialState()}function oe(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",Ge(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(nu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),An.createNotification(t,n),!1}))}function ue(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return Ge(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(nu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),An.createNotification(t,n),!1}))}function ae(t){t.registerStores({pushNotifications:ou})}function se(t,e){return Ge(t,"POST","template",{template:e})}function ce(t){return t.set("isListening",!0)}function fe(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function he(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function le(){return Eu.getInitialState()}function pe(){return Eu.getInitialState()}function _e(){return Eu.getInitialState()}function de(t){return bu[t.hassId]}function ve(t){var e=de(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(gu.VOICE_TRANSMITTING,{finalTranscript:n}),xn.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(gu.VOICE_DONE)}),(function(){t.dispatch(gu.VOICE_ERROR)}))}}function ye(t){var e=de(t);e&&(e.recognition.stop(),bu[t.hassId]=!1)}function ge(t){ve(t),ye(t)}function me(t){var e=ge.bind(null,t);e();var n=new webkitSpeechRecognition;bu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(gu.VOICE_START)},n.onerror=function(){return t.dispatch(gu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=de(t);if(n){for(var r="",i="",o=e.resultIndex;o>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?_(t)+e:e}function v(){return!0}function y(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function g(t,e){return S(t,e,0)}function m(t,e){return S(t,e,e)}function S(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[On]);if("function"==typeof e)return e}function C(t){return t&&"number"==typeof t.length}function D(t){return null===t||void 0===t?U():o(t)?t.toSeq():V(t)}function R(t){return null===t||void 0===t?U().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():H(t)}function z(t){return null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function k(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function P(t){return!(!t||!t[Tn])}function U(){return An||(An=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new N(t).fromEntrySeq():O(t)?new k(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return C(t)?new L(t):w(t)?new N(t):O(t)?new k(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?B(e,t,"",{"":t}):Y(t)}function B(t,e,n,r){return Array.isArray(e)?t.call(r,n,z(e).map((function(n,r){return B(t,n,r,e)}))):J(e)?t.call(r,n,R(e).map((function(n,r){return B(t,n,r,e)}))):e}function Y(t){return Array.isArray(t)?z(t).map(Y).toList():J(t)?R(t).map(Y).toMap():t}function J(t){return t&&(t.constructor===Object||void 0===t.constructor)}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!s(t);if(c(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var h=!0,l=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,yn)):!W(t.get(r,yn),e))return h=!1,!1}));return h&&t.size===l}function Q(t,e){if(!(this instanceof Q))return new Q(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Cn)return Cn;Cn=this}}function Z(t,e){if(!t)throw new Error(e)}function $(t,e,n){if(!(this instanceof $))return new $(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>Pn?ut(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return st(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=xn[t];return void 0===e&&(e=at(t),Hn===Un&&(Hn=0,xn={}),Hn++,xn[t]=e),e}function at(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ht(t){return null===t||void 0===t?bt():lt(t)&&!c(t)?t:bt().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function lt(t){return!(!t||!t[Vn])}function pt(t,e){this.ownerID=t,this.entries=e}function _t(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function dt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&St(t._root)}function mt(t,e){return b(t,e[0],e[1])}function St(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(qn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function bt(){return Fn||(Fn=Et(0))}function It(t,e,n){var r,i;if(t._root){var o=f(gn),u=f(mn);if(r=Ot(t._root,t.__ownerID,0,void 0,e,n,o,u),!u.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new pt(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):bt()}function Ot(t,e,n,r,i,o,u,a){return t?t.update(e,n,r,i,o,u,a):o===yn?t:(h(a),h(u),new yt(e,r,[i,o]))}function wt(t){return t.constructor===yt||t.constructor===vt}function Tt(t,e,n,r,i){if(t.keyHash===r)return new vt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&vn,a=(0===n?r:r>>>n)&vn,s=u===a?[Tt(t,e,n+_n,r,i)]:(o=new yt(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new dt(t,o+1,u)}function Rt(t,e,r){for(var i=[],u=0;u>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function Nt(t,e,n,r){var i=r?t:p(t);return i[e]=n,i}function Pt(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&io?0:o-n,c=u-n;return c>dn&&(c=dn),function(){if(i===c)return Xn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>dn&&(f=dn),function(){for(;;){if(a){var t=a();if(t!==Xn)return t;a=null}if(c===f)return Xn;var o=e?--f:c++;a=n(s&&s[o],r-_n,i+(o<=t.size||e<0)return t.withMutations((function(t){e<0?Wt(t,e).set(0,n):Wt(t,0,e+1).set(e,n)}));e+=t._origin;var r=t._tail,i=t._root,o=f(mn);return e>=Qt(t._capacity)?r=Bt(r,t.__ownerID,0,e,n,o):i=Bt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):Ft(t._origin,t._capacity,t._level,i,r):t}function Bt(t,e,n,r,i,o){var u=r>>>n&vn,a=t&&u0){var c=t&&t.array[u],f=Bt(c,e,n-_n,r,i,o);return f===c?t:(s=Yt(t,e),s.array[u]=f,s)}return a&&t.array[u]===i?t:(h(o),s=Yt(t,e),void 0===i&&u===s.array.length-1?s.array.pop():s.array[u]=i,s)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Vt(t?t.array.slice():[],e)}function Jt(t,e){if(e>=Qt(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&vn],r-=_n;return n}}function Wt(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var r=t.__ownerID||new l,i=t._origin,o=t._capacity,u=i+e,a=void 0===n?o:n<0?o+n:i+n;if(u===i&&a===o)return t;if(u>=a)return t.clear();for(var s=t._level,c=t._root,f=0;u+f<0;)c=new Vt(c&&c.array.length?[void 0,c]:[],r),s+=_n,f+=1<=1<h?new Vt([],r):_;if(_&&p>h&&u_n;y-=_n){var g=h>>>y&vn;v=v.array[g]=Yt(v.array[g],r)}v.array[h>>>_n&vn]=_}if(a=p)u-=p,a-=p,s=_n,c=null,d=d&&d.removeBefore(r,0,u);else if(u>i||p>>s&vn;if(m!==p>>>s&vn)break;m&&(f+=(1<i&&(c=c.removeBefore(r,s,u-f)),c&&pu&&(u=c.size),o(s)||(c=c.map((function(t){return K(t)}))),i.push(c)}return u>t.size&&(t=t.setSize(u)),Lt(t,e,i)}function Qt(t){return t>>_n<<_n}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function $t(t){return lt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Qn||(Qn=te(bt(),Gt()))}function ne(t,e,n){var r,i,o=t._map,u=t._list,a=o.get(e),s=void 0!==a;if(n===yn){if(!s)return t;u.size>=dn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function ae(t){var e=Ce(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=De,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?Sn:En,n)},e}function se(t,e,n){var r=Ce(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function ce(t,e){var n=Ce(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=De,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function fe(t,e,n,r){var i=Ce(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function he(t,e,n){var r=ht().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function le(t,e,n){var r=u(t),i=(c(t)?Zt():ht()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Ae(t);return i.map((function(e){return Oe(t,o(e))}))}function pe(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n===1/0?n=i:n|=0),y(e,n,i))return t;var o=g(e,i),u=m(n,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,n,r);var a,s=u-o;s===s&&(a=s<0?0:s);var c=Ce(t);return c.size=0===a?a:t.size&&a||void 0,!r&&P(t)&&a>=0&&(c.get=function(e,n){return e=d(this,e),e>=0&&ea)return I();var t=i.next();return r||e===En?t:e===Sn?b(e,s-1,void 0,t):b(e,s-1,t.value[1],t)})},c}function _e(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function de(t,e,n,r){var i=Ce(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===Sn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function ve(t,e){var r=u(t),i=[t].concat(e).map((function(t){return o(t)?r&&(t=n(t)):t=r?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===i.length)return t;if(1===i.length){var s=i[0];if(s===t||r&&u(s)||a(t)&&a(s))return s}var c=new L(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),c}function ye(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){function u(t,c){var f=this;t.__iterate((function(t,i){return(!e||c0}function Ie(t,n,r){var i=Ce(t);return i.size=new L(r).map((function(t){return t.size})).min(),i.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(En,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},i.__iteratorUncached=function(t,i){var o=r.map((function(t){return t=e(t),T(i?t.reverse():t)})),u=0,a=!1;return new E(function(){var e;return a||(e=o.map((function(t){return t.next()})),a=e.some((function(t){return t.done}))),a?I():b(t,u++,n.apply(null,e.map((function(t){return t.value}))))})},i}function Oe(t,e){return P(t)?e:t.constructor(e)}function we(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Te(t){return ft(t.size),_(t)}function Ae(t){return u(t)?n:a(t)?r:i}function Ce(t){return Object.create((u(t)?R:a(t)?z:M).prototype)}function De(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):D.prototype.cacheResult.call(this)}function Re(t,e){return t>e?1:te?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=u(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0});return un(i,r)}function un(t,e){return e=zn(e,3432918353),e=zn(e<<15|e>>>-15,461845907),e=zn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=zn(e^e>>>16,2246822507),e=zn(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=a,e.isAssociative=s,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",fn="@@__IMMUTABLE_KEYED__@@",hn="@@__IMMUTABLE_INDEXED__@@",ln="@@__IMMUTABLE_ORDERED__@@",pn="delete",_n=5,dn=1<<_n,vn=dn-1,yn={},gn={value:!1},mn={value:!1},Sn=0,En=1,bn=2,In="function"==typeof Symbol&&Symbol.iterator,On="@@iterator",wn=In||On;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=Sn,E.VALUES=En,E.ENTRIES=bn,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(D,e),D.of=function(){return D(arguments)},D.prototype.toSeq=function(){return this},D.prototype.toString=function(){return this.__toString("Seq {","}")},D.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},D.prototype.__iterate=function(t,e){return F(this,t,e,!0)},D.prototype.__iterator=function(t,e){return G(this,t,e,!0)},t(R,D),R.prototype.toKeyedSeq=function(){return this},t(z,D),z.of=function(){return z(arguments)},z.prototype.toIndexedSeq=function(){return this},z.prototype.toString=function(){return this.__toString("Seq [","]")},z.prototype.__iterate=function(t,e){return F(this,t,e,!1)},z.prototype.__iterator=function(t,e){return G(this,t,e,!1)},t(M,D),M.of=function(){return M(arguments)},M.prototype.toSetSeq=function(){return this},D.isSeq=P,D.Keyed=R,D.Set=M,D.Indexed=z;var Tn="@@__IMMUTABLE_SEQ__@@";D.prototype[Tn]=!0,t(L,z),L.prototype.get=function(t,e){return this.has(t)?this._array[d(this,t)]:e},L.prototype.__iterate=function(t,e){for(var n=this,r=this._array,i=r.length-1,o=0;o<=i;o++)if(t(r[e?i-o:o],o,n)===!1)return o+1;return o},L.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(j,R),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},j.prototype[ln]=!0,t(k,z),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},k.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,z),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(Q,z),Q.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Q.prototype.get=function(t,e){return this.has(t)?this._value:e},Q.prototype.includes=function(t){return W(this._value,t)},Q.prototype.slice=function(t,e){var n=this.size;return y(t,e,n)?this:new Q(this._value,m(e,n)-g(t,n))},Q.prototype.reverse=function(){return this},Q.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Q.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Q.prototype.__iterate=function(t,e){for(var n=this,r=0;r=0&&e=0&&nn?I():b(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:X(this,t)};var Dn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var Rn,zn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Mn=Object.isExtensible,Ln=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),jn="function"==typeof WeakMap;jn&&(Rn=new WeakMap);var kn=0,Nn="__immutablehash__";"function"==typeof Symbol&&(Nn=Symbol(Nn));var Pn=16,Un=255,Hn=0,xn={};t(ht,et),ht.of=function(){var t=sn.call(arguments,0);return bt().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},ht.prototype.toString=function(){return this.__toString("Map {","}")},ht.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ht.prototype.set=function(t,e){return It(this,t,e)},ht.prototype.setIn=function(t,e){return this.updateIn(t,yn,(function(){return e}))},ht.prototype.remove=function(t){return It(this,t,yn)},ht.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return yn}))},ht.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},ht.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=jt(this,ze(t),e,n);return r===yn?void 0:r},ht.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):bt()},ht.prototype.merge=function(){return Rt(this,void 0,arguments)},ht.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return Rt(this,t,e)},ht.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},ht.prototype.mergeDeep=function(){return Rt(this,zt,arguments)},ht.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return Rt(this,Mt(t),e)},ht.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},ht.prototype.sort=function(t){return Zt(Se(this,t))},ht.prototype.sortBy=function(t,e){return Zt(Se(this,e,t))},ht.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ht.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new l)},ht.prototype.asImmutable=function(){return this.__ensureOwner()},ht.prototype.wasAltered=function(){return this.__altered},ht.prototype.__iterator=function(t,e){return new gt(this,t,e)},ht.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},ht.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ht.isMap=lt;var Vn="@@__IMMUTABLE_MAP__@@",qn=ht.prototype;qn[Vn]=!0,qn[pn]=qn.remove,qn.removeIn=qn.deleteIn,pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Gn)return At(t,s,r,i);var _=t&&t===this.ownerID,d=_?s:p(s);return l?a?c===f-1?d.pop():d[c]=d.pop():d[c]=[r,i]:d.push([r,i]),_?(this.entries=d,this):new pt(t,d)}},_t.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&vn),o=this.bitmap;return 0===(o&i)?r:this.nodes[kt(o&i-1)].get(t+_n,e,n,r)},_t.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=1<=Kn)return Dt(t,l,c,a,_);if(f&&!_&&2===l.length&&wt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&wt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?Nt(l,h,_,d):Ut(l,h,d):Pt(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new _t(t,v,y)},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&vn,o=this.nodes[i];return o?o.get(t+_n,e,n,r):r},dt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=i===yn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Ot(f,t,e+_n,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&vn;if(r>=this.array.length)return new Vt([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-_n,n),i===u&&o)return this}if(o&&!i)return this;var a=Yt(this,t);if(!o)for(var s=0;s>>e&vn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-_n,n),i===o&&r===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Wn,Xn={};t(Zt,ht),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=$t,Zt.prototype[ln]=!0,Zt.prototype[pn]=Zt.prototype.remove;var Qn;t(re,R),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=se(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Te(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Te(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},re.prototype[ln]=!0,t(ie,z),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(oe,M),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ue,R),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){we(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ue.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){we(r);var i=o(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=De,t(Me,et),Me.prototype.toString=function(){return this.__toString(je(this)+" {","}")},Me.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Me.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Me.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Le(this,bt()))},Me.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+je(this));if(this._map&&!this._map.has(t)){var n=this._defaultValues[t];if(e===n)return this}var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:Le(this,r)},Me.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Le(this,e)},Me.prototype.wasAltered=function(){return this._map.wasAltered()},Me.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Me.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Me.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Le(this,e,t):(this.__ownerID=t,this._map=e,this)};var Zn=Me.prototype;Zn[pn]=Zn.remove,Zn.deleteIn=Zn.removeIn=qn.removeIn,Zn.merge=qn.merge,Zn.mergeWith=qn.mergeWith,Zn.mergeIn=qn.mergeIn,Zn.mergeDeep=qn.mergeDeep,Zn.mergeDeepWith=qn.mergeDeepWith,Zn.mergeDeepIn=qn.mergeDeepIn,Zn.setIn=qn.setIn,Zn.update=qn.update,Zn.updateIn=qn.updateIn,Zn.withMutations=qn.withMutations,Zn.asMutable=qn.asMutable,Zn.asImmutable=qn.asImmutable,t(Pe,rt),Pe.of=function(){return this(arguments)},Pe.fromKeys=function(t){return this(n(t).keySeq())},Pe.prototype.toString=function(){return this.__toString("Set {","}")},Pe.prototype.has=function(t){return this._map.has(t)},Pe.prototype.add=function(t){return He(this,this._map.set(t,!0))},Pe.prototype.remove=function(t){return He(this,this._map.remove(t))},Pe.prototype.clear=function(){return He(this,this._map.clear())},Pe.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ft(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pop=function(){return this.slice(1)},Be.prototype.unshift=function(){return this.push.apply(this,arguments)},Be.prototype.unshiftAll=function(t){return this.pushAll(t)},Be.prototype.shift=function(){return this.pop.apply(this,arguments)},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):We()},Be.prototype.slice=function(t,e){if(y(t,e,this.size))return this;var n=g(t,this.size),r=m(e,this.size);if(r!==this.size)return nt.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Je(i,o)},Be.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Je(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Be.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},Be.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Be.isStack=Ye;var ir="@@__IMMUTABLE_STACK__@@",or=Be.prototype;or[ir]=!0,or.withMutations=qn.withMutations,or.asMutable=qn.asMutable,or.asImmutable=qn.asImmutable,or.wasAltered=qn.wasAltered;var ur;e.Iterator=E,Xe(e,{toArray:function(){ft(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,n){t[n]=e})),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return ht(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate((function(e,n){t[n]=e})),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return qe(u(this)?this.valueSeq():this)},toSet:function(){return Pe(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Be(u(this)?this.valueSeq():this)},toList:function(){return Ht(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sn.call(arguments,0);return Oe(this,ve(this,t))},includes:function(t){return this.some((function(e){return W(e,t)}))},entries:function(){return this.__iterator(bn)},every:function(t,e){ft(this.size);var n=!0;return this.__iterate((function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1})),n},filter:function(t,e){return Oe(this,fe(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate((function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""})),e},keys:function(){return this.__iterator(Sn)},map:function(t,e){return Oe(this,se(this,t,e))},reduce:function(t,e,n){ft(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate((function(e,o,u){i?(i=!1,r=e):r=t.call(n,r,e,o,u)})),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Oe(this,ce(this,!0))},slice:function(t,e){return Oe(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return Oe(this,Se(this,t))},values:function(){return this.__iterator(En)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return _(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return he(this,t,e)},equals:function(t){return X(this,t)},entrySeq:function(){var t=this;if(t._cache)return new L(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter($e(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate((function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1})),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(v)},flatMap:function(t,e){return Oe(this,ge(this,t,e))},flatten:function(t){return Oe(this,ye(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find((function(e,n){return W(n,t)}),void 0,e)},getIn:function(t,e){for(var n,r=this,i=ze(t);!(n=i.next()).done;){var o=n.value;if(r=r&&r.get?r.get(o,yn):yn,r===yn)return e}return r},groupBy:function(t,e){return le(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey((function(e){return W(e,t)}))},keySeq:function(){return this.toSeq().map(Qe).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return Oe(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return Oe(this,de(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return Oe(this,Se(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return Oe(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return Oe(this,_e(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var ar=e.prototype;ar[cn]=!0,ar[wn]=ar.values,ar.__toJS=ar.toArray,ar.__toStringMapper=en,ar.inspect=ar.toSource=function(){return this.toString()},ar.chain=ar.flatMap,ar.contains=ar.includes,Xe(n,{flip:function(){return Oe(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return Oe(this,this.toSeq().map((function(i,o){return t.call(e,[o,i],r++,n)})).fromEntrySeq())},mapKeys:function(t,e){var n=this;return Oe(this,this.toSeq().flip().map((function(r,i){return t.call(e,r,i,n)})).flip())}});var sr=n.prototype;sr[fn]=!0,sr[wn]=ar.entries,sr.__toJS=ar.toObject,sr.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Xe(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return Oe(this,fe(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return Oe(this,ce(this,!1))},slice:function(t,e){return Oe(this,pe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=g(t,t<0?this.count():this.size);var r=this.slice(0,t);return Oe(this,1===n?r:r.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return Oe(this,ye(this,t,!1))},get:function(t,e){return t=d(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=d(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,m.toFactory)(E),t.exports=e.default},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new C({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return S(t,[n])}))})),m(t)}))}function u(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){var r=t.get("logger");if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var i=t.get("state"),o=t.get("dirtyStores"),u=i.withMutations((function(u){r.dispatchStart(t,e,n),t.get("stores").forEach((function(i,a){var s=u.get(a),c=void 0;try{c=i.handle(s,e,n)}catch(e){throw r.dispatchError(t,e.message),e}if(void 0===c&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw r.dispatchError(t,h),new Error(h)}u.set(a,c),s!==c&&(o=o.add(a))})),r.dispatchEnd(t,u,o,i)})),a=t.set("state",u).set("dirtyStores",o).update("storeStates",(function(t){return S(t,o)}));return m(a)}function s(t,e){var n=[],r=(0,O.toImmutable)({}).withMutations((function(r){(0,A.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=b.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return S(t,n)}))}function c(t,e,n){var r=e;(0,T.isKeyPath)(e)&&(e=(0,w.fromKeyPath)(e));var i=t.get("nextId"),o=(0,w.getStoreDeps)(e),u=b.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,b.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,T.isKeyPath)(e)&&(0,T.isKeyPath)(r)?(0,T.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return S(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,T.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,w.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");var r=t.get("cache"),o=r.lookup(e),u=!o||y(t,o);return u&&(o=g(t,e)),i(o.get("value"),t.update("cache",(function(t){return u?t.miss(e,o):t.hit(e)})))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",b.default.Set())}function y(t,e){var n=e.get("storeStates");return!n.size||n.some((function(e,n){return t.getIn(["storeStates",n])!==e}))}function g(t,e){var n=(0,w.getDeps)(e).map((function(e){return _(t,e).result})),r=(0,w.getComputeFn)(e).apply(null,n),i=(0,w.getStoreDeps)(e),o=(0,O.toImmutable)({}).withMutations((function(e){i.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return(0,I.CacheEntry)({value:r,storeStates:o,dispatchId:t.get("dispatchId")})}function m(t){return t.update("dispatchId",(function(t){return t+1}))}function S(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var E=n(3),b=r(E),I=n(9),O=n(5),w=n(10),T=n(11),A=n(4),C=b.default.Record({result:null,reactorState:null})},function(t,e,n){function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){return new s}Object.defineProperty(e,"__esModule",{value:!0});var o=(function(){function t(t,e){for(var n=0;nn.dispatchId)throw new Error("Refusing to cache older value");return n})))}},{key:"evict",value:function(e){return new t(this.cache.remove(e))}}]),t})();e.BasicCache=s;var c=1e3,f=1,h=(function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1],i=arguments.length<=2||void 0===arguments[2]?new s:arguments[2],o=arguments.length<=3||void 0===arguments[3]?(0,u.OrderedSet)():arguments[3];r(this,t),console.log("using LRU"),this.limit=e,this.evictCount=n,this.cache=i,this.lru=o}return o(t,[{key:"lookup",value:function(t,e){return this.cache.lookup(t,e)}},{key:"has",value:function(t){return this.cache.has(t)}},{key:"asMap",value:function(){return this.cache.asMap()}},{key:"hit",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache,this.lru.remove(e).add(e)):this}},{key:"miss",value:function(e,n){var r;if(this.lru.size>=this.limit){if(this.has(e))return new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.remove(e).add(e));var i=this.lru.take(this.evictCount).reduce((function(t,e){return t.evict(e)}),this.cache).miss(e,n);r=new t(this.limit,this.evictCount,i,this.lru.skip(this.evictCount).add(e))}else r=new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.add(e));return r}},{key:"evict",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache.evict(e),this.lru.remove(e)):this}}]),t})();e.LRUCache=h},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(8),i={dispatchStart:function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},dispatchError:function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},dispatchEnd:function(t,e,n,i){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}};e.ConsoleGroupLogger=i;var o={dispatchStart:function(t,e,n){},dispatchError:function(t,e){},dispatchEnd:function(t,e,n){}};e.NoopLogger=o},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n(9),o=n(12),u=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=u;var a=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=a;var s=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,i.DefaultCache)(),logger:o.NoopLogger,storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:u});e.ReactorState=s;var c=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=c}])}))})),Ie=t(be),Oe=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},we=Oe,Te=we({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),Ae=Ie.Store,Ce=Ie.toImmutable,De=new Ae({getInitialState:function(){return Ce({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Te.VALIDATING_AUTH_TOKEN,n),this.on(Te.VALID_AUTH_TOKEN,r),this.on(Te.INVALID_AUTH_TOKEN,i)}}),Re=Ie.Store,ze=Ie.toImmutable,Me=new Re({getInitialState:function(){return ze({authToken:null,host:""})},initialize:function(){this.on(Te.VALID_AUTH_TOKEN,o),this.on(Te.LOG_OUT,u)}}),Le=Ie.Store,je=new Le({getInitialState:function(){return!0},initialize:function(){this.on(Te.VALID_AUTH_TOKEN,a)}}),ke=we({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Ne=Ie.Store,Pe=Ie.toImmutable,Ue=new Ne({getInitialState:function(){return Pe({isStreaming:!1,hasError:!1})},initialize:function(){this.on(ke.STREAM_START,s),this.on(ke.STREAM_ERROR,c),this.on(ke.LOG_OUT,f)}}),He=e((function(t,e){function n(t){return{type:"auth",api_password:t}}function r(){return{type:"get_states"}}function i(){return{type:"get_config"}}function o(){return{type:"get_services"}}function u(){return{type:"get_panels"}}function a(t,e,n){var r={type:"call_service",domain:t,service:e};return n&&(r.service_data=n),r}function s(t){var e={type:"subscribe_events"};return t&&(e.event_type=t),e}function c(t){return{type:"unsubscribe_events",subscription:t}}function f(){return{type:"ping"}}function h(t){return t.result}function l(t,e){var n=new d(t,e);return n.connect()}Object.defineProperty(e,"__esModule",{value:!0});var p=1,_=2,d=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};d.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},d.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},d.prototype.connect=function(){var t=this;return new Promise(function(e,r){var i=t.commands;Object.keys(i).forEach((function(t){var e=i[t];e.reject&&e.reject()}));var o=!1;t.connectionTries+=1,t.socket=new WebSocket(t.url),t.socket.addEventListener("open",(function(){t.connectionTries=0})),t.socket.addEventListener("message",(function(u){var a=JSON.parse(u.data);switch(console.log("Received",a),a.type){case"event":t.commands[a.id].eventCallback(a.event); -break;case"result":a.success?t.commands[a.id].resolve(a):t.commands[a.id].reject(a.error),delete t.commands[a.id];break;case"pong":break;case"auth_required":t.sendMessage(n(t.options.authToken));break;case"auth_invalid":r({code:_}),o=!0;break;case"auth_ok":e(t),t.fireEvent("ready"),t.commandId=1,t.commands={},Object.keys(i).forEach((function(e){var n=i[e];n.eventType&&t.subscribeEvents(n.eventCallback,n.eventType).then((function(t){n.unsubscribe=t}))}));break;default:console.warn("Unhandled message",a)}})),t.socket.addEventListener("close",(function(){if(!o&&!t.closeRequested){0===t.connectionTries?t.fireEvent("disconnected"):r(p);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},d.prototype.close=function(){this.closeRequested=!0,this.socket.close()},d.prototype.getStates=function(){return this.sendMessagePromise(r()).then(h)},d.prototype.getServices=function(){return this.sendMessagePromise(o()).then(h)},d.prototype.getPanels=function(){return this.sendMessagePromise(u()).then(h)},d.prototype.getConfig=function(){return this.sendMessagePromise(i()).then(h)},d.prototype.callService=function(t,e,n){return this.sendMessagePromise(a(t,e,n))},d.prototype.subscribeEvents=function(t,e){var n=this;return this.sendMessagePromise(s(e)).then((function(r){var i={eventCallback:t,eventType:e,unsubscribe:function(){return n.sendMessagePromise(c(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},d.prototype.ping=function(){return this.sendMessagePromise(f())},d.prototype.sendMessage=function(t){console.log("Sending",t),this.socket.send(JSON.stringify(t))},d.prototype.sendMessagePromise=function(t){var e=this;return new Promise(function(n,r){e.commandId+=1;var i=e.commandId;t.id=i,e.commands[i]={resolve:n,reject:r},e.sendMessage(t)})},e.ERR_CANNOT_CONNECT=p,e.ERR_INVALID_AUTH=_,e.createConnection=l,e.default=l})),xe=He.createConnection,Ve=we({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),qe=Ie.Store,Fe=new qe({getInitialState:function(){return!0},initialize:function(){this.on(Ve.API_FETCH_ALL_START,(function(){return!0})),this.on(Ve.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(Ve.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(Ve.LOG_OUT,(function(){return!1}))}}),Ge=h,Ke=we({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),Be=Ie.Store,Ye=Ie.toImmutable,Je=new Be({getInitialState:function(){return Ye({})},initialize:function(){var t=this;this.on(Ke.API_FETCH_SUCCESS,l),this.on(Ke.API_SAVE_SUCCESS,l),this.on(Ke.API_DELETE_SUCCESS,p),this.on(Ke.LOG_OUT,(function(){return t.getInitialState()}))}}),We=Object.prototype.hasOwnProperty,Xe=Object.prototype.propertyIsEnumerable,Qe=d()?Object.assign:function(t,e){for(var n,r,i=arguments,o=_(t),u=1;u199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function I(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?un({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||un;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function yt(t,e){var n=e.date;return n.toISOString()}function gt(){return Wr.getInitialState()}function mt(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Qr({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],Qr(e.map(En.fromJSON)))}))}))}function St(){return Zr.getInitialState()}function Et(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,ni(e.map(En.fromJSON)))}))}))}function bt(){return ri.getInitialState()}function It(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,r)})),history.length>1&&t.set(ui,r)}))}function Ot(){return ai.getInitialState()}function wt(t,e){t.dispatch(Yr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function Tt(t,e){void 0===e&&(e=null),t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),nn(t,"GET",n).then((function(e){return t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function At(t,e){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_START,{date:e}),nn(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function Ct(t){var e=t.evaluate(fi);return At(t,e)}function Dt(t){t.registerStores({currentEntityHistoryDate:Wr,entityHistory:Zr,isLoadingEntityHistory:ti,recentEntityHistory:ri,recentEntityHistoryUpdated:ai})}function zt(t){t.registerStores({moreInfoEntityId:Kr})}function Rt(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;o0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function Bt(t){var e=ao[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),ao[t.hassId]=!1)}function Yt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),Bt(t);var r=t.evaluate(Do.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",E(i,{authToken:r}).then((function(e){var r=Kt((function(){return e.ping()}),oo);r(),e.socket.addEventListener("message",r),ao[t.hassId]={conn:e,scheduleHealthCheck:r},uo.forEach((function(n){return e.subscribeEvents(io.bind(null,t),n)})),t.batch((function(){t.dispatch(Be.STREAM_START),n&&eo.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(Be.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(Be.STREAM_START),eo.fetchAll(t)}))}))}))}function Jt(t){t.registerStores({streamStatus:We})}function Wt(t,e,n){void 0===n&&(n={});var r=n.rememberAuth;void 0===r&&(r=!1);var i=n.host;void 0===i&&(i=""),t.dispatch(Pe.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),eo.fetchAll(t).then((function(){t.dispatch(Pe.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),lo.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=vo),t.dispatch(Pe.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Xt(t){t.dispatch(Pe.LOG_OUT,{})}function Qt(t){t.registerStores({authAttempt:xe,authCurrent:Fe,rememberAuth:Ke})}function Zt(){if(!("localStorage"in window))return{};var t=window.localStorage,e="___test";try{return t.setItem(e,e),t.removeItem(e),t}catch(t){return{}}}function $t(){var t=new ko({debug:!1});return t.hassId=No++,t}function te(t,e,n){Object.keys(n).forEach((function(r){var i=n[r];if("register"in i&&i.register(e),"getters"in i&&Object.defineProperty(t,r+"Getters",{value:i.getters,enumerable:!0}),"actions"in i){var o={};Object.getOwnPropertyNames(i.actions).forEach((function(t){"function"==typeof i.actions[t]&&Object.defineProperty(o,t,{value:i.actions[t].bind(null,e),enumerable:!0})})),Object.defineProperty(t,r+"Actions",{value:o,enumerable:!0})}}))}function ee(t,e){return Po(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function ne(t){return nn(t,"GET","error_log")}function re(t,e){var n=e.date;return n.toISOString()}function ie(){return Ko.getInitialState()}function oe(t,e){var n=e.date,r=e.entries;return t.set(n,Zo(r.map(Xo.fromJSON)))}function ue(){return $o.getInitialState()}function ae(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function se(){return nu.getInitialState()}function ce(t,e){t.dispatch(Fo.LOGBOOK_DATE_SELECTED,{date:e})}function fe(t,e){t.dispatch(Fo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),nn(t,"GET","logbook/"+e).then((function(n){return t.dispatch(Fo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(Fo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function he(t){return!t||(new Date).getTime()-t>ou}function le(t){t.registerStores({currentLogbookDate:Ko,isLoadingLogbookEntries:Yo,logbookEntries:$o,logbookEntriesUpdated:nu})}function pe(t){return t.set("active",!0)}function _e(t){return t.set("active",!1)}function de(){return yu.getInitialState()}function ve(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",nn(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(_u.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),Hn.createNotification(t,n),!1}))}function ye(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return nn(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(_u.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Hn.createNotification(t,n),!1}))}function ge(t){t.registerStores({pushNotifications:yu})}function me(t,e){return nn(t,"POST","template",{template:e})}function Se(t){return t.set("isListening",!0)}function Ee(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)}))}function be(t,e){var n=e.finalTranscript;return t.withMutations((function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)}))}function Ie(){return Lu.getInitialState()}function Oe(){return Lu.getInitialState()}function we(){return Lu.getInitialState()}function Te(t){return ju[t.hassId]}function Ae(t){var e=Te(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(zu.VOICE_TRANSMITTING,{finalTranscript:n}),Zn.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(zu.VOICE_DONE)}),(function(){t.dispatch(zu.VOICE_ERROR)}))}}function Ce(t){var e=Te(t);e&&(e.recognition.stop(),ju[t.hassId]=!1)}function De(t){Ae(t),Ce(t)}function ze(t){var e=De.bind(null,t);e();var n=new webkitSpeechRecognition;ju[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(zu.VOICE_START)},n.onerror=function(){return t.dispatch(zu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Te(t);if(n){for(var r="",i="",o=e.resultIndex;o>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?_(t)+e:e}function v(){return!0}function y(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function g(t,e){return S(t,e,0)}function m(t,e){return S(t,e,e)}function S(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[On]);if("function"==typeof e)return e}function C(t){return t&&"number"==typeof t.length}function D(t){return null===t||void 0===t?U():o(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?U().toKeyedSeq():o(t)?u(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():o(t)?u(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function k(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function P(t){return!(!t||!t[Tn])}function U(){return An||(An=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new N(t).fromEntrySeq():O(t)?new k(t).fromEntrySeq():"object"==typeof t?new j(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"==typeof t&&new j(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return C(t)?new L(t):w(t)?new N(t):O(t)?new k(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?B(e,t,"",{"":t}):Y(t)}function B(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map((function(n,r){return B(t,n,r,e)}))):J(e)?t.call(r,n,z(e).map((function(n,r){return B(t,n,r,e)}))):e}function Y(t){return Array.isArray(t)?R(t).map(Y).toList():J(t)?z(t).map(Y).toMap():t}function J(t){return t&&(t.constructor===Object||void 0===t.constructor)}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||u(t)!==u(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!s(t);if(c(t)){var r=t.entries();return e.every((function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))}))&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var f=t;t=e,e=f}var h=!0,l=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,yn)):!W(t.get(r,yn),e))return h=!1,!1}));return h&&t.size===l}function Q(t,e){if(!(this instanceof Q))return new Q(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(Cn)return Cn;Cn=this}}function Z(t,e){if(!t)throw new Error(e)}function $(t,e,n){if(!(this instanceof $))return new $(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e>>1&1073741824|3221225471&t}function ot(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>Pn?ut(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return st(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function ut(t){var e=xn[t];return void 0===e&&(e=at(t),Hn===Un&&(Hn=0,xn={}),Hn++,xn[t]=e),e}function at(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ft(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function ht(t){return null===t||void 0===t?bt():lt(t)&&!c(t)?t:bt().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function lt(t){return!(!t||!t[Vn])}function pt(t,e){this.ownerID=t,this.entries=e}function _t(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function dt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&St(t._root)}function mt(t,e){return b(t,e[0],e[1])}function St(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(qn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function bt(){return Fn||(Fn=Et(0))}function It(t,e,n){var r,i;if(t._root){var o=f(gn),u=f(mn);if(r=Ot(t._root,t.__ownerID,0,void 0,e,n,o,u),!u.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new pt(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):bt()}function Ot(t,e,n,r,i,o,u,a){return t?t.update(e,n,r,i,o,u,a):o===yn?t:(h(a),h(u),new yt(e,r,[i,o]))}function wt(t){return t.constructor===yt||t.constructor===vt}function Tt(t,e,n,r,i){if(t.keyHash===r)return new vt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&vn,a=(0===n?r:r>>>n)&vn,s=u===a?[Tt(t,e,n+_n,r,i)]:(o=new yt(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new dt(t,o+1,u)}function zt(t,e,r){for(var i=[],u=0;u>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function Nt(t,e,n,r){var i=r?t:p(t);return i[e]=n,i}function Pt(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;a0&&io?0:o-n,c=u-n;return c>dn&&(c=dn),function(){if(i===c)return Xn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>dn&&(f=dn),function(){for(;;){if(a){var t=a();if(t!==Xn)return t;a=null}if(c===f)return Xn;var o=e?--f:c++;a=n(s&&s[o],r-_n,i+(o<=t.size||e<0)return t.withMutations((function(t){e<0?Wt(t,e).set(0,n):Wt(t,0,e+1).set(e,n)}));e+=t._origin;var r=t._tail,i=t._root,o=f(mn);return e>=Qt(t._capacity)?r=Bt(r,t.__ownerID,0,e,n,o):i=Bt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):Ft(t._origin,t._capacity,t._level,i,r):t}function Bt(t,e,n,r,i,o){var u=r>>>n&vn,a=t&&u0){var c=t&&t.array[u],f=Bt(c,e,n-_n,r,i,o);return f===c?t:(s=Yt(t,e),s.array[u]=f,s)}return a&&t.array[u]===i?t:(h(o),s=Yt(t,e),void 0===i&&u===s.array.length-1?s.array.pop():s.array[u]=i,s)}function Yt(t,e){return e&&t&&e===t.ownerID?t:new Vt(t?t.array.slice():[],e)}function Jt(t,e){if(e>=Qt(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&vn],r-=_n;return n}}function Wt(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var r=t.__ownerID||new l,i=t._origin,o=t._capacity,u=i+e,a=void 0===n?o:n<0?o+n:i+n;if(u===i&&a===o)return t;if(u>=a)return t.clear();for(var s=t._level,c=t._root,f=0;u+f<0;)c=new Vt(c&&c.array.length?[void 0,c]:[],r),s+=_n,f+=1<=1<h?new Vt([],r):_;if(_&&p>h&&u_n;y-=_n){var g=h>>>y&vn;v=v.array[g]=Yt(v.array[g],r)}v.array[h>>>_n&vn]=_}if(a=p)u-=p,a-=p,s=_n,c=null,d=d&&d.removeBefore(r,0,u);else if(u>i||p>>s&vn;if(m!==p>>>s&vn)break;m&&(f+=(1<i&&(c=c.removeBefore(r,s,u-f)),c&&pu&&(u=c.size),o(s)||(c=c.map((function(t){return K(t)}))),i.push(c)}return u>t.size&&(t=t.setSize(u)),Lt(t,e,i)}function Qt(t){return t>>_n<<_n}function Zt(t){return null===t||void 0===t?ee():$t(t)?t:ee().withMutations((function(e){var r=n(t);ft(r.size),r.forEach((function(t,n){return e.set(n,t)}))}))}function $t(t){return lt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Qn||(Qn=te(bt(),Gt()))}function ne(t,e,n){var r,i,o=t._map,u=t._list,a=o.get(e),s=void 0!==a;if(n===yn){if(!s)return t;u.size>=dn&&u.size>=2*o.size?(i=u.filter((function(t,e){return void 0!==t&&a!==e})),r=i.toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function ue(t){this._iter=t,this.size=t.size}function ae(t){var e=Ce(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=De,e.__iterateUncached=function(e,n){var r=this;return t.__iterate((function(t,n){return e(n,t,r)!==!1}),n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?Sn:En,n)},e}function se(t,e,n){var r=Ce(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate((function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1}),i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function ce(t,e){var n=Ce(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=De,n.__iterate=function(e,n){var r=this;return t.__iterate((function(t,n){return e(t,n,r)}),!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function fe(t,e,n,r){var i=Ce(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate((function(t,o,s){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function he(t,e,n){var r=ht().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function le(t,e,n){var r=u(t),i=(c(t)?Zt():ht()).asMutable();t.__iterate((function(o,u){i.update(e.call(n,o,u,t),(function(t){return t=t||[],t.push(r?[u,o]:o),t}))}));var o=Ae(t);return i.map((function(e){return Oe(t,o(e))}))}function pe(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n===1/0?n=i:n|=0),y(e,n,i))return t;var o=g(e,i),u=m(n,i);if(o!==o||u!==u)return pe(t.toSeq().cacheResult(),e,n,r);var a,s=u-o;s===s&&(a=s<0?0:s);var c=Ce(t);return c.size=0===a?a:t.size&&a||void 0,!r&&P(t)&&a>=0&&(c.get=function(e,n){return e=d(this,e),e>=0&&ea)return I();var t=i.next();return r||e===En?t:e===Sn?b(e,s-1,void 0,t):b(e,s-1,t.value[1],t)})},c}function _e(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate((function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)})),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function de(t,e,n,r){var i=Ce(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate((function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return s++,i(t,r?o:s-1,u)})),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===Sn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function ve(t,e){var r=u(t),i=[t].concat(e).map((function(t){return o(t)?r&&(t=n(t)):t=r?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===i.length)return t;if(1===i.length){var s=i[0];if(s===t||r&&u(s)||a(t)&&a(s))return s}var c=new L(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),c}function ye(t,e,n){var r=Ce(t);return r.__iterateUncached=function(r,i){function u(t,c){var f=this;t.__iterate((function(t,i){return(!e||c0}function Ie(t,n,r){var i=Ce(t);return i.size=new L(r).map((function(t){return t.size})).min(),i.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(En,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},i.__iteratorUncached=function(t,i){var o=r.map((function(t){return t=e(t),T(i?t.reverse():t)})),u=0,a=!1;return new E(function(){var e;return a||(e=o.map((function(t){return t.next()})),a=e.some((function(t){return t.done}))),a?I():b(t,u++,n.apply(null,e.map((function(t){return t.value}))))})},i}function Oe(t,e){return P(t)?e:t.constructor(e)}function we(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Te(t){return ft(t.size),_(t)}function Ae(t){return u(t)?n:a(t)?r:i}function Ce(t){return Object.create((u(t)?z:a(t)?R:M).prototype)}function De(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):D.prototype.cacheResult.call(this)}function ze(t,e){return t>e?1:te?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=u(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0});return un(i,r)}function un(t,e){return e=Rn(e,3432918353),e=Rn(e<<15|e>>>-15,461845907),e=Rn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Rn(e^e>>>16,2246822507),e=Rn(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=u,e.isIndexed=a,e.isAssociative=s,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",fn="@@__IMMUTABLE_KEYED__@@",hn="@@__IMMUTABLE_INDEXED__@@",ln="@@__IMMUTABLE_ORDERED__@@",pn="delete",_n=5,dn=1<<_n,vn=dn-1,yn={},gn={value:!1},mn={value:!1},Sn=0,En=1,bn=2,In="function"==typeof Symbol&&Symbol.iterator,On="@@iterator",wn=In||On;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=Sn,E.VALUES=En,E.ENTRIES=bn,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(D,e),D.of=function(){return D(arguments)},D.prototype.toSeq=function(){return this},D.prototype.toString=function(){return this.__toString("Seq {","}")},D.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},D.prototype.__iterate=function(t,e){return F(this,t,e,!0)},D.prototype.__iterator=function(t,e){return G(this,t,e,!0)},t(z,D),z.prototype.toKeyedSeq=function(){return this},t(R,D),R.of=function(){return R(arguments)},R.prototype.toIndexedSeq=function(){return this},R.prototype.toString=function(){return this.__toString("Seq [","]")},R.prototype.__iterate=function(t,e){return F(this,t,e,!1)},R.prototype.__iterator=function(t,e){return G(this,t,e,!1)},t(M,D),M.of=function(){return M(arguments)},M.prototype.toSetSeq=function(){return this},D.isSeq=P,D.Keyed=z,D.Set=M,D.Indexed=R;var Tn="@@__IMMUTABLE_SEQ__@@";D.prototype[Tn]=!0,t(L,R),L.prototype.get=function(t,e){return this.has(t)?this._array[d(this,t)]:e},L.prototype.__iterate=function(t,e){for(var n=this,r=this._array,i=r.length-1,o=0;o<=i;o++)if(t(r[e?i-o:o],o,n)===!1)return o+1;return o},L.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},j.prototype[ln]=!0,t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},k.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(Q,R),Q.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Q.prototype.get=function(t,e){return this.has(t)?this._value:e},Q.prototype.includes=function(t){return W(this._value,t)},Q.prototype.slice=function(t,e){var n=this.size;return y(t,e,n)?this:new Q(this._value,m(e,n)-g(t,n))},Q.prototype.reverse=function(){return this},Q.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Q.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Q.prototype.__iterate=function(t,e){for(var n=this,r=0;r=0&&e=0&&nn?I():b(t,o++,u)})},$.prototype.equals=function(t){return t instanceof $?this._start===t._start&&this._end===t._end&&this._step===t._step:X(this,t)};var Dn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var zn,Rn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-2?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Mn=Object.isExtensible,Ln=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),jn="function"==typeof WeakMap;jn&&(zn=new WeakMap);var kn=0,Nn="__immutablehash__";"function"==typeof Symbol&&(Nn=Symbol(Nn));var Pn=16,Un=255,Hn=0,xn={};t(ht,et),ht.of=function(){var t=sn.call(arguments,0);return bt().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},ht.prototype.toString=function(){return this.__toString("Map {","}")},ht.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},ht.prototype.set=function(t,e){return It(this,t,e)},ht.prototype.setIn=function(t,e){return this.updateIn(t,yn,(function(){return e}))},ht.prototype.remove=function(t){return It(this,t,yn)},ht.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return yn}))},ht.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},ht.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=jt(this,Re(t),e,n);return r===yn?void 0:r},ht.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):bt()},ht.prototype.merge=function(){return zt(this,void 0,arguments)},ht.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return zt(this,t,e)},ht.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},ht.prototype.mergeDeep=function(){return zt(this,Rt,arguments)},ht.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return zt(this,Mt(t),e)},ht.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,bt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},ht.prototype.sort=function(t){return Zt(Se(this,t))},ht.prototype.sortBy=function(t,e){return Zt(Se(this,e,t))},ht.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},ht.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new l)},ht.prototype.asImmutable=function(){return this.__ensureOwner()},ht.prototype.wasAltered=function(){return this.__altered},ht.prototype.__iterator=function(t,e){return new gt(this,t,e)},ht.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate((function(e){return r++,t(e[1],e[0],n)}),e),r},ht.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},ht.isMap=lt;var Vn="@@__IMMUTABLE_MAP__@@",qn=ht.prototype;qn[Vn]=!0,qn[pn]=qn.remove,qn.removeIn=qn.deleteIn,pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Gn)return At(t,s,r,i);var _=t&&t===this.ownerID,d=_?s:p(s);return l?a?c===f-1?d.pop():d[c]=d.pop():d[c]=[r,i]:d.push([r,i]),_?(this.entries=d,this):new pt(t,d)}},_t.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&vn),o=this.bitmap;return 0===(o&i)?r:this.nodes[kt(o&i-1)].get(t+_n,e,n,r)},_t.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=1<=Kn)return Dt(t,l,c,a,_);if(f&&!_&&2===l.length&&wt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&wt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?Nt(l,h,_,d):Ut(l,h,d):Pt(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new _t(t,v,y)},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&vn,o=this.nodes[i];return o?o.get(t+_n,e,n,r):r},dt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&vn,s=i===yn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Ot(f,t,e+_n,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&vn;if(r>=this.array.length)return new Vt([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-_n,n),i===u&&o)return this}if(o&&!i)return this;var a=Yt(this,t);if(!o)for(var s=0;s>>e&vn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-_n,n),i===o&&r===this.array.length-1)return this}var u=Yt(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Wn,Xn={};t(Zt,ht),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=$t,Zt.prototype[ln]=!0,Zt.prototype[pn]=Zt.prototype.remove;var Qn;t(re,z),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=se(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Te(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Te(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},re.prototype[ln]=!0,t(ie,R),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(oe,M),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ue,z),ue.prototype.entrySeq=function(){return this._iter.toSeq()},ue.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){we(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ue.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){we(r);var i=o(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=ue.prototype.cacheResult=De,t(Me,et),Me.prototype.toString=function(){return this.__toString(je(this)+" {","}")},Me.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Me.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Me.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Le(this,bt()))},Me.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+je(this));if(this._map&&!this._map.has(t)){var n=this._defaultValues[t];if(e===n)return this}var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:Le(this,r)},Me.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Le(this,e)},Me.prototype.wasAltered=function(){return this._map.wasAltered()},Me.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Me.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Me.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Le(this,e,t):(this.__ownerID=t,this._map=e,this)};var Zn=Me.prototype;Zn[pn]=Zn.remove,Zn.deleteIn=Zn.removeIn=qn.removeIn,Zn.merge=qn.merge,Zn.mergeWith=qn.mergeWith,Zn.mergeIn=qn.mergeIn,Zn.mergeDeep=qn.mergeDeep,Zn.mergeDeepWith=qn.mergeDeepWith,Zn.mergeDeepIn=qn.mergeDeepIn,Zn.setIn=qn.setIn,Zn.update=qn.update,Zn.updateIn=qn.updateIn,Zn.withMutations=qn.withMutations,Zn.asMutable=qn.asMutable,Zn.asImmutable=qn.asImmutable,t(Pe,rt),Pe.of=function(){return this(arguments)},Pe.fromKeys=function(t){return this(n(t).keySeq())},Pe.prototype.toString=function(){return this.__toString("Set {","}")},Pe.prototype.has=function(t){return this._map.has(t)},Pe.prototype.add=function(t){ +return He(this,this._map.set(t,!0))},Pe.prototype.remove=function(t){return He(this,this._map.remove(t))},Pe.prototype.clear=function(){return He(this,this._map.clear())},Pe.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter((function(t){return 0!==t.size})),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations((function(e){for(var n=0;n=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ft(t.size);var e=this.size,n=this._head;return t.reverse().forEach((function(t){e++,n={value:t,next:n}})),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Je(e,n)},Be.prototype.pop=function(){return this.slice(1)},Be.prototype.unshift=function(){return this.push.apply(this,arguments)},Be.prototype.unshiftAll=function(t){return this.pushAll(t)},Be.prototype.shift=function(){return this.pop.apply(this,arguments)},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):We()},Be.prototype.slice=function(t,e){if(y(t,e,this.size))return this;var n=g(t,this.size),r=m(e,this.size);if(r!==this.size)return nt.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Je(i,o)},Be.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Je(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Be.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},Be.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Be.isStack=Ye;var ir="@@__IMMUTABLE_STACK__@@",or=Be.prototype;or[ir]=!0,or.withMutations=qn.withMutations,or.asMutable=qn.asMutable,or.asImmutable=qn.asImmutable,or.wasAltered=qn.wasAltered;var ur;e.Iterator=E,Xe(e,{toArray:function(){ft(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,n){t[n]=e})),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return ht(this.toKeyedSeq())},toObject:function(){ft(this.size);var t={};return this.__iterate((function(e,n){t[n]=e})),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return qe(u(this)?this.valueSeq():this)},toSet:function(){return Pe(u(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():u(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Be(u(this)?this.valueSeq():this)},toList:function(){return Ht(u(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){var t=sn.call(arguments,0);return Oe(this,ve(this,t))},includes:function(t){return this.some((function(e){return W(e,t)}))},entries:function(){return this.__iterator(bn)},every:function(t,e){ft(this.size);var n=!0;return this.__iterate((function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1})),n},filter:function(t,e){return Oe(this,fe(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ft(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ft(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate((function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""})),e},keys:function(){return this.__iterator(Sn)},map:function(t,e){return Oe(this,se(this,t,e))},reduce:function(t,e,n){ft(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate((function(e,o,u){i?(i=!1,r=e):r=t.call(n,r,e,o,u)})),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Oe(this,ce(this,!0))},slice:function(t,e){return Oe(this,pe(this,t,e,!0))},some:function(t,e){return!this.every($e(t),e)},sort:function(t){return Oe(this,Se(this,t))},values:function(){return this.__iterator(En)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return _(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return he(this,t,e)},equals:function(t){return X(this,t)},entrySeq:function(){var t=this;if(t._cache)return new L(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter($e(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate((function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1})),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(v)},flatMap:function(t,e){return Oe(this,ge(this,t,e))},flatten:function(t){return Oe(this,ye(this,t,!0))},fromEntrySeq:function(){return new ue(this)},get:function(t,e){return this.find((function(e,n){return W(n,t)}),void 0,e)},getIn:function(t,e){for(var n,r=this,i=Re(t);!(n=i.next()).done;){var o=n.value;if(r=r&&r.get?r.get(o,yn):yn,r===yn)return e}return r},groupBy:function(t,e){return le(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey((function(e){return W(e,t)}))},keySeq:function(){return this.toSeq().map(Qe).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return Oe(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return Oe(this,de(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile($e(t),e)},sortBy:function(t,e){return Oe(this,Se(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return Oe(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return Oe(this,_e(this,t,e))},takeUntil:function(t,e){return this.takeWhile($e(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var ar=e.prototype;ar[cn]=!0,ar[wn]=ar.values,ar.__toJS=ar.toArray,ar.__toStringMapper=en,ar.inspect=ar.toSource=function(){return this.toString()},ar.chain=ar.flatMap,ar.contains=ar.includes,Xe(n,{flip:function(){return Oe(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return Oe(this,this.toSeq().map((function(i,o){return t.call(e,[o,i],r++,n)})).fromEntrySeq())},mapKeys:function(t,e){var n=this;return Oe(this,this.toSeq().flip().map((function(r,i){return t.call(e,r,i,n)})).flip())}});var sr=n.prototype;sr[fn]=!0,sr[wn]=ar.entries,sr.__toJS=ar.toObject,sr.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Xe(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return Oe(this,fe(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return Oe(this,ce(this,!1))},slice:function(t,e){return Oe(this,pe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=g(t,t<0?this.count():this.size);var r=this.slice(0,t);return Oe(this,1===n?r:r.concat(p(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return Oe(this,ye(this,t,!1))},get:function(t,e){return t=d(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=d(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!=typeof Int8Array?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c.default.Set().withMutations((function(n){n.union(t.observerState.get("any")),e.forEach((function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)}))}));n.forEach((function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c.default.is(a,s)||i.call(null,s)}}));var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t})();e.default=(0,m.toFactory)(E),t.exports=e.default},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,(function(e,r){n[r]=t.evaluate(e)})),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e.default=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),(function(n,i){var o=t.observe(n,(function(t){e.setState(r({},i,t))}));e.__unwatchFns.push(o)}))},componentWillUnmount:function(){for(var t=this;this.__unwatchFns.length;)t.__unwatchFns.shift()()}}},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){return new C({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",(function(t){return t.set(n,e)})).update("state",(function(t){return t.set(n,r)})).update("dirtyStores",(function(t){return t.add(n)})).update("storeStates",(function(t){return S(t,[n])}))})),m(t)}))}function u(t,e){return t.withMutations((function(t){(0,A.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){var r=t.get("logger");if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var i=t.get("state"),o=t.get("dirtyStores"),u=i.withMutations((function(u){r.dispatchStart(t,e,n),t.get("stores").forEach((function(i,a){var s=u.get(a),c=void 0;try{c=i.handle(s,e,n)}catch(e){throw r.dispatchError(t,e.message),e}if(void 0===c&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw r.dispatchError(t,h),new Error(h)}u.set(a,c),s!==c&&(o=o.add(a))})),r.dispatchEnd(t,u,o,i)})),a=t.set("state",u).set("dirtyStores",o).update("storeStates",(function(t){return S(t,o)}));return m(a)}function s(t,e){var n=[],r=(0,O.toImmutable)({}).withMutations((function(r){(0,A.each)(e,(function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}}))})),i=b.default.Set(n);return t.update("state",(function(t){return t.merge(r)})).update("dirtyStores",(function(t){return t.union(i)})).update("storeStates",(function(t){return S(t,n)}))}function c(t,e,n){var r=e;(0,T.isKeyPath)(e)&&(e=(0,w.fromKeyPath)(e));var i=t.get("nextId"),o=(0,w.getStoreDeps)(e),u=b.default.Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",(function(t){return t.add(i)})):t.withMutations((function(t){o.forEach((function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,b.default.Set()),t.updateIn(["stores",e],(function(t){return t.add(i)}))}))})),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter((function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return!!i&&((0,T.isKeyPath)(e)&&(0,T.isKeyPath)(r)?(0,T.isEqual)(e,r):e===r)}));return t.withMutations((function(t){r.forEach((function(e){return l(t,e)}))}))}function l(t,e){return t.withMutations((function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",(function(t){return t.remove(n)})):r.forEach((function(e){t.updateIn(["stores",e],(function(t){return t?t.remove(n):t}))})),t.removeIn(["observersMap",n])}))}function p(t){var e=t.get("state");return t.withMutations((function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach((function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,O.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)})),t.update("storeStates",(function(t){return S(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,T.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,w.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");var r=t.get("cache"),o=r.lookup(e),u=!o||y(t,o);return u&&(o=g(t,e)),i(o.get("value"),t.update("cache",(function(t){return u?t.miss(e,o):t.hit(e)})))}function d(t){var e={};return t.get("stores").forEach((function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)})),e}function v(t){return t.set("dirtyStores",b.default.Set())}function y(t,e){var n=e.get("storeStates");return!n.size||n.some((function(e,n){return t.getIn(["storeStates",n])!==e}))}function g(t,e){var n=(0,w.getDeps)(e).map((function(e){return _(t,e).result})),r=(0,w.getComputeFn)(e).apply(null,n),i=(0,w.getStoreDeps)(e),o=(0,O.toImmutable)({}).withMutations((function(e){i.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return(0,I.CacheEntry)({value:r,storeStates:o,dispatchId:t.get("dispatchId")})}function m(t){return t.update("dispatchId",(function(t){return t+1}))}function S(t,e){return t.withMutations((function(t){e.forEach((function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)}))}))}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var E=n(3),b=r(E),I=n(9),O=n(5),w=n(10),T=n(11),A=n(4),C=b.default.Record({result:null,reactorState:null})},function(t,e,n){function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(){return new s}Object.defineProperty(e,"__esModule",{value:!0});var o=(function(){function t(t,e){for(var n=0;nn.dispatchId)throw new Error("Refusing to cache older value");return n})))}},{key:"evict",value:function(e){return new t(this.cache.remove(e))}}]),t})();e.BasicCache=s;var c=1e3,f=1,h=(function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1],i=arguments.length<=2||void 0===arguments[2]?new s:arguments[2],o=arguments.length<=3||void 0===arguments[3]?(0,u.OrderedSet)():arguments[3];r(this,t),console.log("using LRU"),this.limit=e,this.evictCount=n,this.cache=i,this.lru=o}return o(t,[{key:"lookup",value:function(t,e){return this.cache.lookup(t,e)}},{key:"has",value:function(t){return this.cache.has(t)}},{key:"asMap",value:function(){return this.cache.asMap()}},{key:"hit",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache,this.lru.remove(e).add(e)):this}},{key:"miss",value:function(e,n){var r;if(this.lru.size>=this.limit){if(this.has(e))return new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.remove(e).add(e));var i=this.lru.take(this.evictCount).reduce((function(t,e){return t.evict(e)}),this.cache).miss(e,n);r=new t(this.limit,this.evictCount,i,this.lru.skip(this.evictCount).add(e))}else r=new t(this.limit,this.evictCount,this.cache.miss(e,n),this.lru.add(e));return r}},{key:"evict",value:function(e){return this.cache.has(e)?new t(this.limit,this.evictCount,this.cache.evict(e),this.lru.remove(e)):this}}]),t})();e.LRUCache=h},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h.default.Set());var n=h.default.Set().withMutations((function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach((function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}}))}));return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map((function(t){return t.first()})).filter((function(t){return!!t}));return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e.default={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e.default},function(t,e,n){function r(t){return t&&t.__esModule?t:{default:t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a.default.List(t),r=a.default.List(e);return a.default.is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(8),i={dispatchStart:function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},dispatchError:function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},dispatchEnd:function(t,e,n,i){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}};e.ConsoleGroupLogger=i;var o={dispatchStart:function(t,e,n){},dispatchError:function(t,e){},dispatchEnd:function(t,e,n){}};e.NoopLogger=o},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=n(9),o=n(12),u=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=u;var a=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=a;var s=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,i.DefaultCache)(),logger:o.NoopLogger,storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:u});e.ReactorState=s;var c=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=c}])}))})),je=t(Le),ke=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},Ne=ke,Pe=Ne({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),Ue=je.Store,He=je.toImmutable,xe=new Ue({getInitialState:function(){return He({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Pe.VALIDATING_AUTH_TOKEN,n),this.on(Pe.VALID_AUTH_TOKEN,r),this.on(Pe.INVALID_AUTH_TOKEN,i)}}),Ve=je.Store,qe=je.toImmutable,Fe=new Ve({getInitialState:function(){return qe({authToken:null,host:""})},initialize:function(){this.on(Pe.VALID_AUTH_TOKEN,o),this.on(Pe.LOG_OUT,u)}}),Ge=je.Store,Ke=new Ge({getInitialState:function(){return!0},initialize:function(){this.on(Pe.VALID_AUTH_TOKEN,a)}}),Be=Ne({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Ye=je.Store,Je=je.toImmutable,We=new Ye({getInitialState:function(){return Je({isStreaming:!1,hasError:!1})},initialize:function(){this.on(Be.STREAM_START,s),this.on(Be.STREAM_ERROR,c),this.on(Be.LOG_OUT,f)}}),Xe=1,Qe=2,Ze=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};Ze.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},Ze.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},Ze.prototype.connect=function(){var t=this;return new Promise(function(e,n){var r=t.commands;Object.keys(r).forEach((function(t){var e=r[t];e.reject&&e.reject()}));var i=!1;t.connectionTries+=1,t.socket=new WebSocket(t.url),t.socket.addEventListener("open",(function(){t.connectionTries=0})),t.socket.addEventListener("message",(function(o){var u=JSON.parse(o.data);switch(u.type){case"event":t.commands[u.id].eventCallback(u.event);break;case"result":u.success?t.commands[u.id].resolve(u):t.commands[u.id].reject(u.error), +delete t.commands[u.id];break;case"pong":break;case"auth_required":t.sendMessage(h(t.options.authToken));break;case"auth_invalid":n({code:Qe}),i=!0;break;case"auth_ok":e(t),t.fireEvent("ready"),t.commandId=1,t.commands={},Object.keys(r).forEach((function(e){var n=r[e];n.eventType&&t.subscribeEvents(n.eventCallback,n.eventType).then((function(t){n.unsubscribe=t}))}))}})),t.socket.addEventListener("close",(function(){if(!i&&!t.closeRequested){0===t.connectionTries?t.fireEvent("disconnected"):n(Xe);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},Ze.prototype.close=function(){this.closeRequested=!0,this.socket.close()},Ze.prototype.getStates=function(){return this.sendMessagePromise(l()).then(S)},Ze.prototype.getServices=function(){return this.sendMessagePromise(_()).then(S)},Ze.prototype.getPanels=function(){return this.sendMessagePromise(d()).then(S)},Ze.prototype.getConfig=function(){return this.sendMessagePromise(p()).then(S)},Ze.prototype.callService=function(t,e,n){return this.sendMessagePromise(v(t,e,n))},Ze.prototype.subscribeEvents=function(t,e){var n=this;return this.sendMessagePromise(y(e)).then((function(r){var i={eventCallback:t,eventType:e,unsubscribe:function(){return n.sendMessagePromise(g(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},Ze.prototype.ping=function(){return this.sendMessagePromise(m())},Ze.prototype.sendMessage=function(t){this.socket.send(JSON.stringify(t))},Ze.prototype.sendMessagePromise=function(t){var e=this;return new Promise(function(n,r){e.commandId+=1;var i=e.commandId;t.id=i,e.commands[i]={resolve:n,reject:r},e.sendMessage(t)})};var $e=Ne({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),tn=je.Store,en=new tn({getInitialState:function(){return!0},initialize:function(){this.on($e.API_FETCH_ALL_START,(function(){return!0})),this.on($e.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on($e.API_FETCH_ALL_FAIL,(function(){return!1})),this.on($e.LOG_OUT,(function(){return!1}))}}),nn=b,rn=Ne({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),on=je.Store,un=je.toImmutable,an=new on({getInitialState:function(){return un({})},initialize:function(){var t=this;this.on(rn.API_FETCH_SUCCESS,I),this.on(rn.API_SAVE_SUCCESS,I),this.on(rn.API_DELETE_SUCCESS,O),this.on(rn.LOG_OUT,(function(){return t.getInitialState()}))}}),sn=Object.prototype.hasOwnProperty,cn=Object.prototype.propertyIsEnumerable,fn=T()?Object.assign:function(t,e){for(var n,r,i=arguments,o=w(t),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 C`R5)0 literal 33404 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,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(e<0)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;t \ No newline at end of file +this.currentTarget=t,this.defaultPrevented=!1,this.eventPhase=Event.AT_TARGET,this.timeStamp=Date.now()},i=window.Element.prototype.animate;window.Element.prototype.animate=function(n,r){var o=i.call(this,n,r);o._cancelHandlers=[],o.oncancel=null;var a=o.cancel;o.cancel=function(){a.call(this);var i=new e(this,null,t()),n=this._cancelHandlers.concat(this.oncancel?[this.oncancel]:[]);setTimeout(function(){n.forEach(function(t){t.call(i.target,i)})},0)};var s=o.addEventListener;o.addEventListener=function(t,e){"function"==typeof e&&"cancel"==t?this._cancelHandlers.push(e):s.call(this,t,e)};var u=o.removeEventListener;return o.removeEventListener=function(t,e){if("cancel"==t){var i=this._cancelHandlers.indexOf(e);i>=0&&this._cancelHandlers.splice(i,1)}else u.call(this,t,e)},o}}}(),function(t){var e=document.documentElement,i=null,n=!1;try{var r=getComputedStyle(e).getPropertyValue("opacity"),o="0"==r?"1":"0";i=e.animate({opacity:[o,o]},{duration:1}),i.currentTime=0,n=getComputedStyle(e).getPropertyValue("opacity")==o}catch(t){}finally{i&&i.cancel()}if(!n){var a=window.Element.prototype.animate;window.Element.prototype.animate=function(e,i){return window.Symbol&&Symbol.iterator&&Array.prototype.from&&e[Symbol.iterator]&&(e=Array.from(e)),Array.isArray(e)||null===e||(e=t.convertToArrayForm(e)),a.call(this,e,i)}}}(c),!function(t,e,i){function n(t){var i=e.timeline;i.currentTime=t,i._discardAnimations(),0==i._animations.length?o=!1:requestAnimationFrame(n)}var r=window.requestAnimationFrame;window.requestAnimationFrame=function(t){return r(function(i){e.timeline._updateAnimationsPromises(),t(i),e.timeline._updateAnimationsPromises()})},e.AnimationTimeline=function(){this._animations=[],this.currentTime=void 0},e.AnimationTimeline.prototype={getAnimations:function(){return this._discardAnimations(),this._animations.slice()},_updateAnimationsPromises:function(){e.animationsWithPromises=e.animationsWithPromises.filter(function(t){return t._updatePromises()})},_discardAnimations:function(){this._updateAnimationsPromises(),this._animations=this._animations.filter(function(t){return"finished"!=t.playState&&"idle"!=t.playState})},_play:function(t){var i=new e.Animation(t,this);return this._animations.push(i),e.restartWebAnimationsNextTick(),i._updatePromises(),i._animation.play(),i._updatePromises(),i},play:function(t){return t&&t.remove(),this._play(t)}};var o=!1;e.restartWebAnimationsNextTick=function(){o||(o=!0,requestAnimationFrame(n))};var a=new e.AnimationTimeline;e.timeline=a;try{Object.defineProperty(window.document,"timeline",{configurable:!0,get:function(){return a}})}catch(t){}try{window.document.timeline=a}catch(t){}}(c,e,f),function(t,e,i){e.animationsWithPromises=[],e.Animation=function(e,i){if(this.id="",e&&e._id&&(this.id=e._id),this.effect=e,e&&(e._animation=this),!i)throw new Error("Animation with null timeline is not supported");this._timeline=i,this._sequenceNumber=t.sequenceNumber++,this._holdTime=0,this._paused=!1,this._isGroup=!1,this._animation=null,this._childAnimations=[],this._callback=null,this._oldPlayState="idle",this._rebuildUnderlyingAnimation(),this._animation.cancel(),this._updatePromises()},e.Animation.prototype={_updatePromises:function(){var t=this._oldPlayState,e=this.playState;return this._readyPromise&&e!==t&&("idle"==e?(this._rejectReadyPromise(),this._readyPromise=void 0):"pending"==t?this._resolveReadyPromise():"pending"==e&&(this._readyPromise=void 0)),this._finishedPromise&&e!==t&&("idle"==e?(this._rejectFinishedPromise(),this._finishedPromise=void 0):"finished"==e?this._resolveFinishedPromise():"finished"==t&&(this._finishedPromise=void 0)),this._oldPlayState=this.playState,this._readyPromise||this._finishedPromise},_rebuildUnderlyingAnimation:function(){this._updatePromises();var t,i,n,r,o=!!this._animation;o&&(t=this.playbackRate,i=this._paused,n=this.startTime,r=this.currentTime,this._animation.cancel(),this._animation._wrapper=null,this._animation=null),(!this.effect||this.effect instanceof window.KeyframeEffect)&&(this._animation=e.newUnderlyingAnimationForKeyframeEffect(this.effect),e.bindAnimationForKeyframeEffect(this)),(this.effect instanceof window.SequenceEffect||this.effect instanceof window.GroupEffect)&&(this._animation=e.newUnderlyingAnimationForGroup(this.effect),e.bindAnimationForGroup(this)),this.effect&&this.effect._onsample&&e.bindAnimationForCustomEffect(this),o&&(1!=t&&(this.playbackRate=t),null!==n?this.startTime=n:null!==r?this.currentTime=r:null!==this._holdTime&&(this.currentTime=this._holdTime),i&&this.pause()),this._updatePromises()},_updateChildren:function(){if(this.effect&&"idle"!=this.playState){var t=this.effect._timing.delay;this._childAnimations.forEach(function(i){this._arrangeChildren(i,t),this.effect instanceof window.SequenceEffect&&(t+=e.groupChildDuration(i.effect))}.bind(this))}},_setExternalAnimation:function(t){if(this.effect&&this._isGroup)for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 76d65cc903b46fe3bbdba06c91b97b1c2cc3a06f..f67f4d68a9ed1c23329e7b0d387d74aaa8edff09 100644 GIT binary patch delta 19280 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 H;9vj%PPM8k delta 19136 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�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 diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 24dd4357d85..9184aaf33f7 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 24dd4357d85d598146a5a3f815ba6bd1ada0a428 +Subproject commit 9184aaf33f72db0421ad4eda90a72a91c49400aa diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index 4f93b83631b..51805ebd91d 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ -

\ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index 3b51da813668d51f0b656a45eb358b1481bdde6c..f6c74234e4d56e43ca9be97211a2b65756a5e07c 100644 GIT binary patch delta 17498 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 AX8-^I delta 17234 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
  • h8dWVk8j*0-q0^xc%F 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 diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 9690aa3e87b..bf02b53455b 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","e695d1590c2c8e64a29f06d92710aded"],["/frontend/panels/dev-event-c2d5ec676be98d4474d19f94d0262c1e.html","6c55fc819751923ab00c62ae3fbb7222"],["/frontend/panels/dev-info-ec613406ce7e20d93754233d55625c8a.html","8e28a4c617fd6963b45103d5e5c80617"],["/frontend/panels/dev-service-4a051878b92b002b8b018774ba207769.html","57123d199ea22cbaaddc46c36b18075f"],["/frontend/panels/dev-state-65e5f791cc467561719bf591f1386054.html","78158786a6597ef86c3fd6f4985cde92"],["/frontend/panels/dev-template-7d744ab7f7c08b6d6ad42069989de400.html","8a6ee994b1cdb45b081299b8609915ed"],["/frontend/panels/map-1bf6965b24d76db71a1871865cd4a3a2.html","a74c01c2ee68c83c9938af067ec33b81"],["/static/core-525498104891894d97cbf0caf7291edc.js","8c68a52b771138f031ae5a8d7d21e175"],["/static/frontend-18667e347b85a368724308bb1b9485b4.html","1d1d01b472225c87e12af68f0d690747"],["/static/mdi-46a76f877ac9848899b8ed382427c16f.html","a846c4082dd5cffd88ac72cbe943e691"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.toString().match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n))return e.add(new Request(n,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);var a="index.html";!t&&a&&(n=addDirectoryIndex(n,a),t=urlsToCacheKeys.has(n));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n0~f%S=v9Z_+x*pWn=8;-Whk}kE`B~(S0bA_m|y{b6@N%*~@$->c=Bu>d2McznpSzy$>*8Pc|8L@~^2XkgbpFf{nTKemJ|NDDvX2q){qb{<) zbuwvAWxkZgmv!N2PF2~n9@+bWD$MIr)Lg}$Stc(O@|0`s5j?o`+>YXRkKOK;q^
  • VKGk!Ps=B1NhuBdie&?BP zOi8^_i#4p)EniV$>m;hOEK#e`QDl`)8fWjL2vtuh)k)pGQhb8qBId?xFX{9a&zc~x zoiUHA@av9QVzauP4r%n8UeM`Oo_WGRP4c?a^9=13EzfoCW-U@#Y_z=aoCc@h!H|WE zTC@Z+k9LOjXBZtipy4{(Nx%bancSMHeSAxVGNSt&dw{;56d zGG~2s(K;c=c~P(3Q9)v=;zU7*Q>y)`(~icx zFi2H+Z1H@_^#n$(ClS|pdnRdwX4HGI1{_P3;p*A1V&WCZb=Bm@wWytvzQTG*(~71Q z^K^BHPMzr5&B5{5cBeGEo00DnHy4E`O-BRS1cU@PO6Yb8EXn0~Wa1gTD9N>h%`q}ZZV zIpdhU@Yh{4&Yq1DJ1yMnmnHPHLDf51woPDS%&~|SDvRtVe`O2V7`3J~Q^7FckdseA zPlu0zwi8p&#u&y^|7LFBH;A6PDAHSa#$TP&A(NgvDgRn2X6fm2PBh_0=bfoxH}=_H zdvwUIXT$412lgM_H0$ft*{w4QIRvFtHmv+0VC(Y4*@?ru?-b_;`9F3?-c5euWpIV- zn(l!`ClybrUTeHkEg;Hm*euq$ATVB>qxVNwY9;I0>owc9xg9ZiDPl^eVG|wW(t9$Di*DcxA4V;ypiny%wwevU20Lr>Wk4Nm-d6KDS># z5k9%@jYE~wHqnDYGb&pzHH*y@xFh(n&G~!Y&y*~ic$Q`VZUoMIefQTU#+>TAGrs8W zxOMp%_x@L=r<{A6vn-}qPbx^-v?Q+5+-xFhm22tO4j*&=*AasH8HII*dP+}a=i6Ve zUe+h;R2sPGS-aUR2KT;auij0)xu>dWE_akn!1+VP>!nlsU;PRbzv>qHzh~Lq)AM&` zJeOswomq2trSS3Ni97#>k{MIrl{)BfmGx zJv&}v-+DLezS@~b>M70tBDW^*(LbMWn}6x#;a#tveY$S8QtQYS&4Rw8g;ze7>oz|K z5_wjf+I8uzO$PIt_owcfbvBp?XLtUE*5fly%%1V`uf~_mE2bB+x>6gLTsfaIS-oP@YMp~V{28aW-gzi5e9e1u z@RGHD9Ws1#xzgKa`c9~opD5IJxOv_Cd6xTkzuT;y=l{jLV$zS&Ym)m9@NV&ledV3_tlrMQ zdfHC?dNx^pxw>{s#@jh_W>f~p^TnC@_DX)q@N}qk{enWDU%y#+t=7;R9XA8 z?0M7?S1%o(%*@{c&kpaN`7ZD1!JPI-O#D@^wI!-wZ`!wXqy4v6Pv31jTD|68d49$Q z{~NFSox}U@D_Gt&zb3Gu@BQ8HCpP6Bw2gQdSeq_g@};z3tl4cf~mi zvJ3P!`d*oS&iQ-F-Iyog!e>O+>?~0Fw@vbz+M_2&4zF1^;WKN|zR-@d453!LrmYsM zKEL2rN{whq_B!)}`etc2qJCdmc`wJh=JCIs1qyY)A6A$M?AYCXc&+(Q`Jk)vJyS&( F7yz07YPSFY From 77d568dc475f438c301d657a2c1c858e2fd1971e Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 28 Nov 2016 07:29:21 +0100 Subject: [PATCH 085/137] Fixed incorrect event-order (#4605) --- homeassistant/components/logbook.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 49ab709f8f5..94445935093 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -139,9 +139,10 @@ class LogbookView(HomeAssistantView): def get_results(): """Query DB for results.""" events = recorder.get_model('Events') - query = recorder.query('Events').filter( - (events.time_fired > start_day) & - (events.time_fired < end_day)) + query = recorder.query('Events').order_by( + events.time_fired).filter( + (events.time_fired > start_day) & + (events.time_fired < end_day)) events = recorder.execute(query) return _exclude_events(events, self.config) From 3b9d5cdf730d310962624170f4e02df05715e914 Mon Sep 17 00:00:00 2001 From: Valentin Alexeev Date: Mon, 28 Nov 2016 09:42:57 +0200 Subject: [PATCH 086/137] DuneHD media player (#4588) * Implement WAQI sensor * Corrections based on CI check. * Updated requirements_all.txt for pwaqi==1.2 * Require latest version of pwaqi * Initial implementation of DuneHD media player component based on pdunehd. * Major: avoid update() in property fetch, Major: implement source support, Major: single device per media player instance, Major: support for volume / mute controls * Pythonify pdunehd. Support media_title. * Fix pylint. * Further pylint. * docstring * Formatting and indentation. * Change indentation to spaces. * Update coverage and recorded requirements before PR. * Further pylint / fake8 / pydocstyle fixes. * Implement next / prev track, Properly decode blu-ray playback, Attempt to decode media title * Fix play / pause Linting * Update requirements. Fix lint. * Fix lint and syntax error * Yet more linting. * Yet more linting. * Fix lint: line too long. * Force update of HA state. --- .coveragerc | 2 + .../components/media_player/dunehd.py | 171 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 176 insertions(+) create mode 100644 homeassistant/components/media_player/dunehd.py diff --git a/.coveragerc b/.coveragerc index 66fd541fcaf..c7d1e4237f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -188,6 +188,7 @@ omit = homeassistant/components/media_player/denon.py homeassistant/components/media_player/denonavr.py homeassistant/components/media_player/directv.py + homeassistant/components/media_player/dunehd.py homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py @@ -309,6 +310,7 @@ omit = homeassistant/components/sensor/waqi.py homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py + homeassistant/components/sensor/waqi.py homeassistant/components/switch/acer_projector.py homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/arest.py diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py new file mode 100644 index 00000000000..7c28ff1190d --- /dev/null +++ b/homeassistant/components/media_player/dunehd.py @@ -0,0 +1,171 @@ +""" +DuneHD implementation of the media player. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/dunehd/ +""" +from homeassistant.components.media_player import ( + SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_ON, STATE_PLAYING) + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +REQUIREMENTS = ['pdunehd==1.3'] + +DEFAULT_NAME = "DuneHD" + +CONF_SOURCES = "sources" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SOURCES): cv.ordered_dict(cv.string, cv.string), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +DUNEHD_PLAYER_SUPPORT = \ + SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the media player demo platform.""" + sources = config.get(CONF_SOURCES, {}) + + from pdunehd import DuneHDPlayer + add_devices([DuneHDPlayerEntity( + DuneHDPlayer(config[CONF_HOST]), + config[CONF_NAME], + sources)]) + + +class DuneHDPlayerEntity(MediaPlayerDevice): + """Implementation of the Dune HD player.""" + + def __init__(self, player, name, sources): + """Setup entity to control Dune HD.""" + self._player = player + self._name = name + self._sources = sources + self._media_title = None + self._state = None + self.update() + + def update(self): + """Update internal status of the entity.""" + self._state = self._player.update_state() + self.__update_title() + return True + + @property + def state(self): + """Return player state.""" + state = STATE_OFF + if 'playback_position' in self._state: + state = STATE_PLAYING + if self._state['player_state'] in ('playing', 'buffering'): + state = STATE_PLAYING + if int(self._state.get('playback_speed', 1234)) == 0: + state = STATE_PAUSED + if self._state['player_state'] == 'navigator': + state = STATE_ON + return state + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return int(self._state.get('playback_volume', 0)) / 100 + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return int(self._state.get('playback_mute', 0)) == 1 + + @property + def source_list(self): + """List of available input sources.""" + return list(self._sources.keys()) + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return DUNEHD_PLAYER_SUPPORT + + def volume_up(self): + """Volume up media player.""" + self._state = self._player.volume_up() + + def volume_down(self): + """Volume down media player.""" + self._state = self._player.volume_down() + + def mute_volume(self, mute): + """Mute/unmute player volume.""" + self._state = self._player.mute(mute) + + def turn_off(self): + """Turn off media player.""" + self._media_title = None + self._state = self._player.turn_off() + self.schedule_update_ha_state() + + def turn_on(self): + """Turn off media player.""" + self._state = self._player.turn_on() + self.schedule_update_ha_state() + + def media_play(self): + """Play media media player.""" + self._state = self._player.play() + self.schedule_update_ha_state() + + def media_pause(self): + """Pause media player.""" + self._state = self._player.pause() + self.schedule_update_ha_state() + + @property + def media_title(self): + """Current media source.""" + self.__update_title() + if self._media_title: + return self._media_title + return self._state.get('playback_url', 'Not playing') + + def __update_title(self): + if self._state['player_state'] == 'bluray_playback': + self._media_title = 'Blu-Ray' + elif 'playback_url' in self._state: + sources = self._sources + sval = sources.values() + skey = sources.keys() + pburl = self._state['playback_url'] + if pburl in sval: + self._media_title = list(skey)[list(sval).index(pburl)] + else: + self._media_title = pburl + + def select_source(self, source): + """Select input source.""" + self._media_title = source + self._state = self._player.launch_media_url(self._sources.get(source)) + self.schedule_update_ha_state() + + def media_previous_track(self): + """Send previous track command.""" + self._state = self._player.previous_track() + self.schedule_update_ha_state() + + def media_next_track(self): + """Send next track command.""" + self._state = self._player.next_track() + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 8406b293780..dd91d9e2ed4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -307,6 +307,9 @@ paho-mqtt==1.2 # homeassistant.components.media_player.panasonic_viera panasonic_viera==0.2 +# homeassistant.components.media_player.dunehd +pdunehd==1.3 + # homeassistant.components.device_tracker.aruba # homeassistant.components.device_tracker.asuswrt # homeassistant.components.device_tracker.cisco_ios From b4841a17a643d8f39940e0b22d83b7a8b8d7d05a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 28 Nov 2016 18:43:47 +0100 Subject: [PATCH 087/137] Hotfix device_tracker yaml config (#4611) --- homeassistant/components/device_tracker/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b00a1044ad6..f985e21ec22 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -291,7 +291,7 @@ class DeviceTracker(object): This method is a coroutine. """ with (yield from self._is_updating): - self.hass.loop.run_in_executor( + yield from self.hass.loop.run_in_executor( None, update_config, self.hass.config.path(YAML_DEVICES), dev_id, device) From 4bc37bd661895d7206097e244b4d55549dbd9bcc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 Nov 2016 20:49:01 +0100 Subject: [PATCH 088/137] Add timeout to request, update ordering, make dev info message shorter, and (#4613) update the other logger messages --- homeassistant/components/updater.py | 80 +++++++++++++++++------------ 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index d4eb5d2211c..a4203876348 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -4,34 +4,39 @@ Support to check for available updates. For more details about this component, please refer to the documentation at https://home-assistant.io/components/updater/ """ -from datetime import datetime, timedelta -import logging import json +import logging +import os import platform import uuid -import os -# pylint: disable=no-name-in-module,import-error +from datetime import datetime, timedelta +# pylint: disable=no-name-in-module, import-error from distutils.version import StrictVersion import requests import voluptuous as vol +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.const import __version__ as CURRENT_VERSION from homeassistant.const import ATTR_FRIENDLY_NAME -import homeassistant.util.dt as dt_util from homeassistant.helpers import event -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) -UPDATER_URL = 'https://updater.home-assistant.io/' -DOMAIN = 'updater' -ENTITY_ID = 'updater.updater' -ATTR_RELEASE_NOTES = 'release_notes' -UPDATER_UUID_FILE = '.uuid' -CONF_REPORTING = 'reporting' REQUIREMENTS = ['distro==1.0.1'] +_LOGGER = logging.getLogger(__name__) + +ATTR_RELEASE_NOTES = 'release_notes' + +CONF_REPORTING = 'reporting' + +DOMAIN = 'updater' + +ENTITY_ID = 'updater.updater' + +UPDATER_URL = 'https://updater.home-assistant.io/' +UPDATER_UUID_FILE = '.uuid' + CONFIG_SCHEMA = vol.Schema({DOMAIN: { vol.Optional(CONF_REPORTING, default=True): cv.boolean }}, extra=vol.ALLOW_EXTRA) @@ -41,12 +46,12 @@ def _create_uuid(hass, filename=UPDATER_UUID_FILE): """Create UUID and save it in a file.""" with open(hass.config.path(filename), 'w') as fptr: _uuid = uuid.uuid4().hex - fptr.write(json.dumps({"uuid": _uuid})) + fptr.write(json.dumps({'uuid': _uuid})) return _uuid def _load_uuid(hass, filename=UPDATER_UUID_FILE): - """Load UUID from a file, or return None.""" + """Load UUID from a file or return None.""" try: with open(hass.config.path(filename)) as fptr: jsonf = json.loads(fptr.read()) @@ -58,12 +63,11 @@ def _load_uuid(hass, filename=UPDATER_UUID_FILE): def setup(hass, config): - """Setup the updater component.""" + """Set up the updater component.""" if 'dev' in CURRENT_VERSION: # This component only makes sense in release versions - _LOGGER.warning(('Updater component enabled in dev. ' - 'You will not receive notifications of new ' - 'versions but analytics will be submitted.')) + _LOGGER.warning("Updater component enabled in 'dev'. " + "No notifications but analytics will be submitted") config = config.get(DOMAIN, {}) huuid = _load_uuid(hass) if config.get(CONF_REPORTING) else None @@ -85,24 +89,29 @@ def check_newest_version(hass, huuid): return if StrictVersion(newest) > StrictVersion(CURRENT_VERSION): - _LOGGER.info('The latest available version is %s.', newest) + _LOGGER.info("The latest available version is %s", newest) hass.states.set( ENTITY_ID, newest, {ATTR_FRIENDLY_NAME: 'Update Available', ATTR_RELEASE_NOTES: releasenotes} ) elif StrictVersion(newest) == StrictVersion(CURRENT_VERSION): - _LOGGER.info('You are on the latest version (%s) of Home Assistant.', + _LOGGER.info("You are on the latest version (%s) of Home Assistant", newest) def get_newest_version(huuid): """Get the newest Home Assistant version.""" - info_object = {'uuid': huuid, 'version': CURRENT_VERSION, - 'timezone': dt_util.DEFAULT_TIME_ZONE.zone, - 'os_name': platform.system(), "arch": platform.machine(), - 'python_version': platform.python_version(), - 'virtualenv': (os.environ.get('VIRTUAL_ENV') is not None), - 'docker': False, 'dev': ('dev' in CURRENT_VERSION)} + info_object = { + 'arch': platform.machine(), + 'dev': ('dev' in CURRENT_VERSION), + 'docker': False, + 'os_name': platform.system(), + 'python_version': platform.python_version(), + 'timezone': dt_util.DEFAULT_TIME_ZONE.zone, + 'uuid': huuid, + 'version': CURRENT_VERSION, + 'virtualenv': (os.environ.get('VIRTUAL_ENV') is not None), + } if platform.system() == 'Windows': info_object['os_version'] = platform.win32_ver()[0] @@ -121,17 +130,20 @@ def get_newest_version(huuid): info_object = {} try: - req = requests.post(UPDATER_URL, json=info_object) + req = requests.post(UPDATER_URL, json=info_object, timeout=5) res = req.json() - _LOGGER.info(('Submitted analytics to Home Assistant servers. ' - 'Information submitted includes %s'), info_object) + _LOGGER.info(("Submitted analytics to Home Assistant servers. " + "Information submitted includes %s"), info_object) return (res['version'], res['release-notes']) except requests.RequestException: - _LOGGER.exception('Could not contact HASS Update to check for updates') + _LOGGER.exception("Could not contact Home Assistant Update to check" + "for updates") return None except ValueError: - _LOGGER.exception('Received invalid response from HASS Update') + _LOGGER.exception("Received invalid response from Home Assistant" + "Update") return None except KeyError: - _LOGGER.exception('Response from HASS Update did not include version') + _LOGGER.exception("Response from Home Assistant Update did not" + "include version") return None From e8367f245a544ce3586c28e4c5e765e4d002b326 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 Nov 2016 20:50:42 +0100 Subject: [PATCH 089/137] Update ordering and sync logger messages (#4615) --- homeassistant/components/notify/__init__.py | 45 ++++++++++----------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index fb016e20617..b9d595401b5 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -4,35 +4,37 @@ Provides functionality to notify people. For more details about this component, please refer to the documentation at https://home-assistant.io/components/notify/ """ -from functools import partial import logging import os +from functools import partial import voluptuous as vol import homeassistant.bootstrap as bootstrap -from homeassistant.config import load_yaml_config_file -from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv +from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.helpers import config_per_platform from homeassistant.util import slugify -DOMAIN = "notify" - -# Title of notification -ATTR_TITLE = "title" -ATTR_TITLE_DEFAULT = "Home Assistant" - -# Target of the notification (user, device, etc) -ATTR_TARGET = 'target' - -# Text to notify user of -ATTR_MESSAGE = "message" +_LOGGER = logging.getLogger(__name__) # Platform specific data ATTR_DATA = 'data' -SERVICE_NOTIFY = "notify" +# Text to notify user of +ATTR_MESSAGE = 'message' + +# Target of the notification (user, device, etc) +ATTR_TARGET = 'target' + +# Title of notification +ATTR_TITLE = 'title' +ATTR_TITLE_DEFAULT = "Home Assistant" + +DOMAIN = 'notify' + +SERVICE_NOTIFY = 'notify' PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): cv.string, @@ -46,8 +48,6 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_DATA): dict, }) -_LOGGER = logging.getLogger(__name__) - def send_message(hass, message, title=None, data=None): """Send a notification message.""" @@ -78,7 +78,7 @@ def setup(hass, config): hass, config, DOMAIN, platform) if notify_implementation is None: - _LOGGER.error("Unknown notification service specified.") + _LOGGER.error("Unknown notification service specified") continue notify_service = notify_implementation.get_service(hass, p_config) @@ -114,7 +114,7 @@ def setup(hass, config): if hasattr(notify_service, 'targets'): platform_name = (p_config.get(CONF_NAME) or platform) for name, target in notify_service.targets.items(): - target_name = slugify("{}_{}".format(platform_name, name)) + target_name = slugify('{}_{}'.format(platform_name, name)) targets[target_name] = target hass.services.register(DOMAIN, target_name, service_call_handler, @@ -124,10 +124,9 @@ def setup(hass, config): platform_name = (p_config.get(CONF_NAME) or SERVICE_NOTIFY) platform_name_slug = slugify(platform_name) - hass.services.register(DOMAIN, platform_name_slug, - service_call_handler, - descriptions.get(SERVICE_NOTIFY), - schema=NOTIFY_SERVICE_SCHEMA) + hass.services.register( + DOMAIN, platform_name_slug, service_call_handler, + descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA) success = True return success From ad4ec49f9c6354dfb4c8d8a65a9e2d90e79a3d45 Mon Sep 17 00:00:00 2001 From: Charles Spirakis Date: Mon, 28 Nov 2016 22:59:46 -0800 Subject: [PATCH 090/137] Update color names to follow w3.org list. (#4374) The color names -> rgb dictionary now follows the color names listed in the w3.org site for css3, section 4.3. Extended color keywords: https://www.w3.org/TR/2010/PR-css3-color-20101028/#svg-color --- homeassistant/util/color.py | 172 +++++++++++++++++++++++++++++++++--- tests/util/test_color.py | 14 ++- 2 files changed, 172 insertions(+), 14 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index e9671c77328..9502849e1d9 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -8,24 +8,170 @@ _LOGGER = logging.getLogger(__name__) HASS_COLOR_MAX = 500 # mireds (inverted) HASS_COLOR_MIN = 154 + +# Official CSS3 colors from w3.org: +# https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 +# names do not have spaces in them so that we can compare against +# reuqests more easily (by removing spaces from the requests as well). +# This lets "dark seagreen" and "dark sea green" both match the same +# color "darkseagreen". COLORS = { - 'white': (255, 255, 255), 'beige': (245, 245, 220), - 'tan': (210, 180, 140), 'gray': (128, 128, 128), - 'navy blue': (0, 0, 128), 'royal blue': (8, 76, 158), - 'blue': (0, 0, 255), 'azure': (0, 127, 255), 'aqua': (127, 255, 212), - 'teal': (0, 128, 128), 'green': (0, 255, 0), - 'forest green': (34, 139, 34), 'olive': (128, 128, 0), - 'chartreuse': (127, 255, 0), 'lime': (191, 255, 0), - 'golden': (255, 215, 0), 'red': (255, 0, 0), 'coral': (0, 63, 72), - 'hot pink': (252, 15, 192), 'fuchsia': (255, 119, 255), - 'lavender': (181, 126, 220), 'indigo': (75, 0, 130), - 'maroon': (128, 0, 0), 'crimson': (220, 20, 60)} + 'aliceblue': (240, 248, 255), + 'antiquewhite': (250, 235, 215), + 'aqua': (0, 255, 255), + 'aquamarine': (127, 255, 212), + 'azure': (240, 255, 255), + 'beige': (245, 245, 220), + 'bisque': (255, 228, 196), + 'black': (0, 0, 0), + 'blanchedalmond': (255, 235, 205), + 'blue': (0, 0, 255), + 'blueviolet': (138, 43, 226), + 'brown': (165, 42, 42), + 'burlywood': (222, 184, 135), + 'cadetblue': (95, 158, 160), + 'chartreuse': (127, 255, 0), + 'chocolate': (210, 105, 30), + 'coral': (255, 127, 80), + 'cornflowerblue': (100, 149, 237), + 'cornsilk': (255, 248, 220), + 'crimson': (220, 20, 60), + 'cyan': (0, 255, 255), + 'darkblue': (0, 0, 139), + 'darkcyan': (0, 139, 139), + 'darkgoldenrod': (184, 134, 11), + 'darkgray': (169, 169, 169), + 'darkgreen': (0, 100, 0), + 'darkgrey': (169, 169, 169), + 'darkkhaki': (189, 183, 107), + 'darkmagenta': (139, 0, 139), + 'darkolivegreen': (85, 107, 47), + 'darkorange': (255, 140, 0), + 'darkorchid': (153, 50, 204), + 'darkred': (139, 0, 0), + 'darksalmon': (233, 150, 122), + 'darkseagreen': (143, 188, 143), + 'darkslateblue': (72, 61, 139), + 'darkslategray': (47, 79, 79), + 'darkslategrey': (47, 79, 79), + 'darkturquoise': (0, 206, 209), + 'darkviolet': (148, 0, 211), + 'deeppink': (255, 20, 147), + 'deepskyblue': (0, 191, 255), + 'dimgray': (105, 105, 105), + 'dimgrey': (105, 105, 105), + 'dodgerblue': (30, 144, 255), + 'firebrick': (178, 34, 34), + 'floralwhite': (255, 250, 240), + 'forestgreen': (34, 139, 34), + 'fuchsia': (255, 0, 255), + 'gainsboro': (220, 220, 220), + 'ghostwhite': (248, 248, 255), + 'gold': (255, 215, 0), + 'goldenrod': (218, 165, 32), + 'gray': (128, 128, 128), + 'green': (0, 128, 0), + 'greenyellow': (173, 255, 47), + 'grey': (128, 128, 128), + 'honeydew': (240, 255, 240), + 'hotpink': (255, 105, 180), + 'indianred': (205, 92, 92), + 'indigo': (75, 0, 130), + 'ivory': (255, 255, 240), + 'khaki': (240, 230, 140), + 'lavender': (230, 230, 250), + 'lavenderblush': (255, 240, 245), + 'lawngreen': (124, 252, 0), + 'lemonchiffon': (255, 250, 205), + 'lightblue': (173, 216, 230), + 'lightcoral': (240, 128, 128), + 'lightcyan': (224, 255, 255), + 'lightgoldenrodyellow': (250, 250, 210), + 'lightgray': (211, 211, 211), + 'lightgreen': (144, 238, 144), + 'lightgrey': (211, 211, 211), + 'lightpink': (255, 182, 193), + 'lightsalmon': (255, 160, 122), + 'lightseagreen': (32, 178, 170), + 'lightskyblue': (135, 206, 250), + 'lightslategray': (119, 136, 153), + 'lightslategrey': (119, 136, 153), + 'lightsteelblue': (176, 196, 222), + 'lightyellow': (255, 255, 224), + 'lime': (0, 255, 0), + 'limegreen': (50, 205, 50), + 'linen': (250, 240, 230), + 'magenta': (255, 0, 255), + 'maroon': (128, 0, 0), + 'mediumaquamarine': (102, 205, 170), + 'mediumblue': (0, 0, 205), + 'mediumorchid': (186, 85, 211), + 'mediumpurple': (147, 112, 219), + 'mediumseagreen': (60, 179, 113), + 'mediumslateblue': (123, 104, 238), + 'mediumspringgreen': (0, 250, 154), + 'mediumturquoise': (72, 209, 204), + 'mediumvioletredred': (199, 21, 133), + 'midnightblue': (25, 25, 112), + 'mintcream': (245, 255, 250), + 'mistyrose': (255, 228, 225), + 'moccasin': (255, 228, 181), + 'navajowhite': (255, 222, 173), + 'navy': (0, 0, 128), + 'navyblue': (0, 0, 128), + 'oldlace': (253, 245, 230), + 'olive': (128, 128, 0), + 'olivedrab': (107, 142, 35), + 'orange': (255, 165, 0), + 'orangered': (255, 69, 0), + 'orchid': (218, 112, 214), + 'palegoldenrod': (238, 232, 170), + 'palegreen': (152, 251, 152), + 'paleturquoise': (175, 238, 238), + 'palevioletred': (219, 112, 147), + 'papayawhip': (255, 239, 213), + 'peachpuff': (255, 218, 185), + 'peru': (205, 133, 63), + 'pink': (255, 192, 203), + 'plum': (221, 160, 221), + 'powderblue': (176, 224, 230), + 'purple': (128, 0, 128), + 'red': (255, 0, 0), + 'rosybrown': (188, 143, 143), + 'royalblue': (65, 105, 225), + 'saddlebrown': (139, 69, 19), + 'salmon': (250, 128, 114), + 'sandybrown': (244, 164, 96), + 'seagreen': (46, 139, 87), + 'seashell': (255, 245, 238), + 'sienna': (160, 82, 45), + 'silver': (192, 192, 192), + 'skyblue': (135, 206, 235), + 'slateblue': (106, 90, 205), + 'slategray': (112, 128, 144), + 'slategrey': (112, 128, 144), + 'snow': (255, 250, 250), + 'springgreen': (0, 255, 127), + 'steelblue': (70, 130, 180), + 'tan': (210, 180, 140), + 'teal': (0, 128, 128), + 'thistle': (216, 191, 216), + 'tomato': (255, 99, 71), + 'turquoise': (64, 224, 208), + 'violet': (238, 130, 238), + 'wheat': (245, 222, 179), + 'white': (255, 255, 255), + 'whitesmoke': (245, 245, 245), + 'yellow': (255, 255, 0), + 'yellowgreen': (154, 205, 50), +} def color_name_to_rgb(color_name): """Convert color name to RGB hex value.""" - hex_value = COLORS.get(color_name.lower()) - + # COLORS map has no spaces in it, so make the color_name have no + # spaces in it as well for matching purposes + hex_value = COLORS.get(color_name.replace(' ', '').lower()) if not hex_value: _LOGGER.error('unknown color supplied %s default to white', color_name) hex_value = COLORS['white'] diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 50bee79283e..e4048cd3cde 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -67,9 +67,21 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0, 0, 255), color_util.color_name_to_rgb('blue')) - self.assertEqual((0, 255, 0), + self.assertEqual((0, 128, 0), color_util.color_name_to_rgb('green')) + # spaces in the name + self.assertEqual((72, 61, 139), + color_util.color_name_to_rgb('dark slate blue')) + + # spaces removed from name + self.assertEqual((72, 61, 139), + color_util.color_name_to_rgb('darkslateblue')) + self.assertEqual((72, 61, 139), + color_util.color_name_to_rgb('dark slateblue')) + self.assertEqual((72, 61, 139), + color_util.color_name_to_rgb('darkslate blue')) + def test_color_name_to_rgb_unknown_name_default_white(self): """Test color_name_to_rgb.""" self.assertEqual((255, 255, 255), From 154c69a454724502388335ec584f74c5713cd610 Mon Sep 17 00:00:00 2001 From: Valentin Alexeev Date: Tue, 29 Nov 2016 09:11:21 +0200 Subject: [PATCH 091/137] Bump version of pwaqi module to 1.3. Fixes #4595. (#4610) --- homeassistant/components/sensor/waqi.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 3acb507f86d..d66810e7c5d 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -12,7 +12,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers import config_validation as cv import voluptuous as vol -REQUIREMENTS = ["pwaqi==1.2"] +REQUIREMENTS = ["pwaqi==1.3"] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index dd91d9e2ed4..bf8340e1239 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -346,7 +346,7 @@ pushbullet.py==0.10.0 pushetta==1.0.15 # homeassistant.components.sensor.waqi -pwaqi==1.2 +pwaqi==1.3 # homeassistant.components.sensor.cpuspeed py-cpuinfo==0.2.3 From 6ddbb4d568c001b6559b320b474ab998d5844aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Br=C3=A6dstrup?= Date: Tue, 29 Nov 2016 17:40:51 +0100 Subject: [PATCH 092/137] Improved exception handling for D-Link switch (#4633) --- homeassistant/components/switch/dlink.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index 6a00fe71f20..3e1f7db3ddb 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN REQUIREMENTS = ['https://github.com/LinuxChristian/pyW215/archive/' - 'v0.3.6.zip#pyW215==0.3.6'] + 'v0.3.7.zip#pyW215==0.3.7'] _LOGGER = logging.getLogger(__name__) @@ -78,7 +78,7 @@ class SmartPlugSwitch(SwitchDevice): TEMP_CELSIUS) temperature = "%i %s" % \ (ui_temp, self.units.temperature_unit) - except ValueError: + except (ValueError, TypeError): temperature = STATE_UNKNOWN try: diff --git a/requirements_all.txt b/requirements_all.txt index bf8340e1239..cfddeefb83b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ https://github.com/Danielhiversen/flux_led/archive/0.9.zip#flux_led==0.9 https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0 # homeassistant.components.switch.dlink -https://github.com/LinuxChristian/pyW215/archive/v0.3.6.zip#pyW215==0.3.6 +https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv From 66473120aba0a083cd02238fdc29a3311b8a8b5d Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Tue, 29 Nov 2016 16:45:04 +0000 Subject: [PATCH 093/137] Add test for delay on automations (#4630) --- tests/components/automation/test_init.py | 57 +++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 2459542b629..e06984e9f7d 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,5 +1,6 @@ """The tests for the automation component.""" import unittest +from datetime import timedelta from unittest.mock import patch from homeassistant.core import callback @@ -9,7 +10,8 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import get_test_home_assistant, assert_setup_component, \ + fire_time_changed class TestAutomation(unittest.TestCase): @@ -85,6 +87,59 @@ class TestAutomation(unittest.TestCase): assert state is not None assert state.attributes.get('entity_id') == ('automation.hello',) + def test_action_delay(self): + """Test action delay.""" + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': [ + { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.platform }} - ' + '{{ trigger.event.event_type }}' + } + }, + {'delay': {'minutes': '10'}}, + { + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.platform }} - ' + '{{ trigger.event.event_type }}' + } + }, + ] + } + }) + + time = dt_util.utcnow() + + with patch('homeassistant.components.automation.utcnow', + return_value=time): + self.hass.bus.fire('test_event') + self.hass.block_till_done() + + assert len(self.calls) == 1 + assert self.calls[0].data['some'] == 'event - test_event' + + future = dt_util.utcnow() + timedelta(minutes=10) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + assert len(self.calls) == 2 + assert self.calls[1].data['some'] == 'event - test_event' + + state = self.hass.states.get('automation.hello') + assert state is not None + assert state.attributes.get('last_triggered') == time + state = self.hass.states.get('group.all_automations') + assert state is not None + assert state.attributes.get('entity_id') == ('automation.hello',) + def test_service_specify_entity_id(self): """Test service data.""" assert setup_component(self.hass, automation.DOMAIN, { From 2d02baf3d0937d2c43957d05ab8458ad8fc4c214 Mon Sep 17 00:00:00 2001 From: DaveSergeant Date: Tue, 29 Nov 2016 08:50:12 -0800 Subject: [PATCH 094/137] Default dimmable brightness to 255 from 100 (#4621) * Default dimmable brightness to 255 from 100 Full brightness for ISY dimmers is 255. The current 100 value turns dimmer switches on to just under half brightness. Probably just an oversight from the Sept implementation. * Brightness change for turn_on, ramp for turn_off. Per discussion with Teagan42 and jbcodemonkey, the brightness should rightfully be None and not an explicit value. There is a continuing issue that the ISY modules don't respect HA's brightness customization values. A new issue will be opened for this. Additionally, turn_off was using ISY's fastoff() which didn't respect the ramping time. The default behavior should just be off(). --- homeassistant/components/light/isy994.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index d8a6f558865..952c52b2809 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -61,10 +61,10 @@ class ISYLightDevice(isy.ISYDevice, Light): def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" - if not self._node.fastoff(): + if not self._node.off(): _LOGGER.debug('Unable to turn on light.') - def turn_on(self, brightness=100, **kwargs) -> None: + def turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" if not self._node.on(val=brightness): _LOGGER.debug('Unable to turn on light.') From 17f0fb69bd9449cdafce9e236e342929fdcc6b5b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 29 Nov 2016 20:53:02 +0100 Subject: [PATCH 095/137] =?UTF-8?q?Homematic=20update=20with=20HomematicIP?= =?UTF-8?q?/HomematicWired=20support=20and=20multible=E2=80=A6=20(#4568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Homematic update with HomematicIP/HomematicWired support and multible connections * fix bug in virtualkey service * create new service & cleanups * fix lint * Pump pyhomematic 0.1.18 --- .../components/binary_sensor/homematic.py | 7 +- homeassistant/components/climate/homematic.py | 7 +- homeassistant/components/cover/homematic.py | 7 +- homeassistant/components/homematic.py | 464 +++++++++++------- homeassistant/components/light/homematic.py | 7 +- homeassistant/components/sensor/homematic.py | 7 +- homeassistant/components/services.yaml | 33 +- homeassistant/components/switch/homematic.py | 7 +- requirements_all.txt | 2 +- 9 files changed, 357 insertions(+), 184 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 35550d15bc8..33eda1f2b1a 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.homematic/ import logging from homeassistant.const import STATE_UNKNOWN from homeassistant.components.binary_sensor import BinarySensorDevice -import homeassistant.components.homematic as homematic +from homeassistant.components.homematic import HMDevice +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -32,14 +33,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return + homematic = get_component("homematic") return homematic.setup_hmdevice_discovery_helper( + hass, HMBinarySensor, discovery_info, add_callback_devices ) -class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): +class HMBinarySensor(HMDevice, BinarySensorDevice): """Representation of a binary Homematic device.""" @property diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index 9be4e7a4886..d0d91ac7270 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.homematic/ """ import logging -import homeassistant.components.homematic as homematic from homeassistant.components.climate import ClimateDevice, STATE_AUTO +from homeassistant.components.homematic import HMDevice from homeassistant.util.temperature import convert from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE +from homeassistant.loader import get_component DEPENDENCIES = ['homematic'] @@ -29,14 +30,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return + homematic = get_component("homematic") return homematic.setup_hmdevice_discovery_helper( + hass, HMThermostat, discovery_info, add_callback_devices ) -class HMThermostat(homematic.HMDevice, ClimateDevice): +class HMThermostat(HMDevice, ClimateDevice): """Representation of a Homematic thermostat.""" @property diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index 189c501aad5..93dfc3a24f8 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -12,7 +12,8 @@ import logging from homeassistant.const import STATE_UNKNOWN from homeassistant.components.cover import CoverDevice,\ ATTR_POSITION -import homeassistant.components.homematic as homematic +from homeassistant.components.homematic import HMDevice +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -24,14 +25,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return + homematic = get_component("homematic") return homematic.setup_hmdevice_discovery_helper( + hass, HMCover, discovery_info, add_callback_devices ) -class HMCover(homematic.HMDevice, CoverDevice): +class HMCover(HMDevice, CoverDevice): """Represents a Homematic Cover in Home Assistant.""" @property diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 42124c643b9..bf38b12237e 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -13,9 +13,9 @@ from functools import partial import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, - CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, - ATTR_ENTITY_ID) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD, + CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import discovery @@ -23,13 +23,10 @@ from homeassistant.config import load_yaml_config_file from homeassistant.util import Throttle DOMAIN = 'homematic' -REQUIREMENTS = ["pyhomematic==0.1.16"] - -HOMEMATIC = None -HOMEMATIC_LINK_DELAY = 0.5 +REQUIREMENTS = ["pyhomematic==0.1.18"] MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300) -MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=60) +MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=30) DISCOVER_SWITCHES = 'homematic.switch' DISCOVER_LIGHTS = 'homematic.light' @@ -44,12 +41,15 @@ ATTR_CHANNEL = 'channel' ATTR_NAME = 'name' ATTR_ADDRESS = 'address' ATTR_VALUE = 'value' +ATTR_PROXY = 'proxy' EVENT_KEYPRESS = 'homematic.keypress' EVENT_IMPULSE = 'homematic.impulse' SERVICE_VIRTUALKEY = 'virtualkey' -SERVICE_SET_VALUE = 'set_value' +SERVICE_RECONNECT = 'reconnect' +SERVICE_SET_VAR_VALUE = 'set_var_value' +SERVICE_SET_DEV_VALUE = 'set_dev_value' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ @@ -109,44 +109,60 @@ CONF_RESOLVENAMES_OPTIONS = [ False ] +DATA_HOMEMATIC = 'homematic' +DATA_DELAY = 'homematic_delay' +DATA_DEVINIT = 'homematic_devinit' +DATA_STORE = 'homematic_store' + CONF_LOCAL_IP = 'local_ip' CONF_LOCAL_PORT = 'local_port' -CONF_REMOTE_IP = 'remote_ip' -CONF_REMOTE_PORT = 'remote_port' +CONF_IP = 'ip' +CONF_PORT = 'port' CONF_RESOLVENAMES = 'resolvenames' -CONF_DELAY = 'delay' CONF_VARIABLES = 'variables' +CONF_DEVICES = 'devices' +CONF_DELAY = 'delay' +CONF_PRIMARY = 'primary' DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False -DEFAULT_REMOTE_PORT = 2001 +DEFAULT_PORT = 2001 DEFAULT_USERNAME = "Admin" DEFAULT_PASSWORD = "" DEFAULT_VARIABLES = False +DEFAULT_DEVICES = True DEFAULT_DELAY = 0.5 +DEFAULT_PRIMARY = False DEVICE_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): "homematic", vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_ADDRESS): cv.string, + vol.Required(ATTR_PROXY): cv.string, vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int), vol.Optional(ATTR_PARAM): cv.string, }) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_REMOTE_IP): cv.string, + vol.Required(CONF_HOSTS): {cv.match_all: { + vol.Required(CONF_IP): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): + cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): + cv.boolean, + vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): + vol.In(CONF_RESOLVENAMES_OPTIONS), + vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean, + vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean, + }}, vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string, vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port, - vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port, - vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): - vol.In(CONF_RESOLVENAMES_OPTIONS), - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float), - vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -154,105 +170,155 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({ vol.Required(ATTR_ADDRESS): cv.string, vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_PARAM): cv.string, + vol.Optional(ATTR_PROXY): cv.string, }) -SCHEMA_SERVICE_SET_VALUE = vol.Schema({ +SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_VALUE): cv.match_all, }) +SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({ + vol.Required(ATTR_ADDRESS): cv.string, + vol.Required(ATTR_CHANNEL): vol.Coerce(int), + vol.Required(ATTR_PARAM): cv.string, + vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_PROXY): cv.string, +}) -def virtualkey(hass, address, channel, param): +SCHEMA_SERVICE_RECONNECT = vol.Schema({}) + + +def virtualkey(hass, address, channel, param, proxy=None): """Send virtual keypress to homematic controlller.""" data = { ATTR_ADDRESS: address, ATTR_CHANNEL: channel, ATTR_PARAM: param, + ATTR_PROXY: proxy, } hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) -def set_value(hass, entity_id, value): +def set_var_value(hass, entity_id, value): """Change value of homematic system variable.""" data = { ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value, } - hass.services.call(DOMAIN, SERVICE_SET_VALUE, data) + hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data) + + +def set_dev_value(hass, address, channel, param, value, proxy=None): + """Send virtual keypress to homematic controlller.""" + data = { + ATTR_ADDRESS: address, + ATTR_CHANNEL: channel, + ATTR_PARAM: param, + ATTR_VALUE: value, + ATTR_PROXY: proxy, + } + + hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data) + + +def reconnect(hass): + """Reconnect to CCU/Homegear.""" + hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) # pylint: disable=unused-argument def setup(hass, config): """Setup the Homematic component.""" - global HOMEMATIC, HOMEMATIC_LINK_DELAY from pyhomematic import HMConnection component = EntityComponent(_LOGGER, DOMAIN, hass) - local_ip = config[DOMAIN].get(CONF_LOCAL_IP) - local_port = config[DOMAIN].get(CONF_LOCAL_PORT) - remote_ip = config[DOMAIN].get(CONF_REMOTE_IP) - remote_port = config[DOMAIN].get(CONF_REMOTE_PORT) - resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES) - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - HOMEMATIC_LINK_DELAY = config[DOMAIN].get(CONF_DELAY) - use_variables = config[DOMAIN].get(CONF_VARIABLES) + hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY) + hass.data[DATA_DEVINIT] = {} + hass.data[DATA_STORE] = [] - if remote_ip is None or local_ip is None: - _LOGGER.error("Missing remote CCU/Homegear or local address") - return False + # create hosts list for pyhomematic + remotes = {} + hosts = {} + for rname, rconfig in config[DOMAIN][CONF_HOSTS].items(): + server = rconfig.get(CONF_IP) + + remotes[rname] = {} + remotes[rname][CONF_IP] = server + remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT) + remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES) + remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME) + remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD) + + if server not in hosts or rconfig.get(CONF_PRIMARY): + hosts[server] = { + CONF_VARIABLES: rconfig.get(CONF_VARIABLES), + CONF_NAME: rname, + } + hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES) # Create server thread bound_system_callback = partial(_system_callback_handler, hass, config) - HOMEMATIC = HMConnection(local=local_ip, - localport=local_port, - remote=remote_ip, - remoteport=remote_port, - systemcallback=bound_system_callback, - resolvenames=resolvenames, - rpcusername=username, - rpcpassword=password, - interface_id="homeassistant") + hass.data[DATA_HOMEMATIC] = HMConnection( + local=config[DOMAIN].get(CONF_LOCAL_IP), + localport=config[DOMAIN].get(CONF_LOCAL_PORT), + remotes=remotes, + systemcallback=bound_system_callback, + interface_id="homeassistant" + ) # Start server thread, connect to peer, initialize to receive events - HOMEMATIC.start() + hass.data[DATA_HOMEMATIC].start() # Stops server when Homeassistant is shutting down - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop) + hass.bus.listen_once( + EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop) hass.config.components.append(DOMAIN) + # init homematic hubs + hub_entities = [] + for _, hub_data in hosts.items(): + hub_entities.append(HMHub(hass, component, hub_data[CONF_NAME], + hub_data[CONF_VARIABLES])) + component.add_entities(hub_entities) + # regeister homematic services descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_VIRTUALKEY, - _hm_service_virtualkey, - descriptions[DOMAIN][SERVICE_VIRTUALKEY], - schema=SCHEMA_SERVICE_VIRTUALKEY) + def _hm_service_virtualkey(service): + """Service handle virtualkey services.""" + address = service.data.get(ATTR_ADDRESS) + channel = service.data.get(ATTR_CHANNEL) + param = service.data.get(ATTR_PARAM) - entities = [] + # device not found + hmdevice = _device_from_servicecall(hass, service) + if hmdevice is None: + _LOGGER.error("%s not found for service virtualkey!", address) + return - ## - # init HM variable - variables = HOMEMATIC.getAllSystemVariables() if use_variables else {} - hm_var_store = {} - if variables is not None: - for key, value in variables.items(): - varia = HMVariable(key, value) - hm_var_store.update({key: varia}) - entities.append(varia) + # if param exists for this device + if param not in hmdevice.ACTIONNODE: + _LOGGER.error("%s not datapoint in hm device %s", param, address) + return - # add homematic entites - entities.append(HMHub(hm_var_store, use_variables)) - component.add_entities(entities) + # channel exists? + if channel not in hmdevice.ACTIONNODE[param]: + _LOGGER.error("%i is not a channel in hm device %s", + channel, address) + return - ## - # register set_value service if exists variables - if not variables: - return True + # call key + hmdevice.actionNodeData(param, True, channel) + + hass.services.register( + DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey, + descriptions[DOMAIN][SERVICE_VIRTUALKEY], + schema=SCHEMA_SERVICE_VIRTUALKEY) def _service_handle_value(service): """Set value on homematic variable object.""" @@ -261,12 +327,43 @@ def setup(hass, config): value = service.data[ATTR_VALUE] for hm_variable in variable_list: - hm_variable.hm_set(value) + if isinstance(hm_variable, HMVariable): + hm_variable.hm_set(value) - hass.services.register(DOMAIN, SERVICE_SET_VALUE, - _service_handle_value, - descriptions[DOMAIN][SERVICE_SET_VALUE], - schema=SCHEMA_SERVICE_SET_VALUE) + hass.services.register( + DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value, + descriptions[DOMAIN][SERVICE_SET_VAR_VALUE], + schema=SCHEMA_SERVICE_SET_VAR_VALUE) + + def _service_handle_reconnect(service): + """Reconnect to all homematic hubs.""" + hass.data[DATA_HOMEMATIC].reconnect() + + hass.services.register( + DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect, + descriptions[DOMAIN][SERVICE_RECONNECT], + schema=SCHEMA_SERVICE_RECONNECT) + + def _service_handle_device(service): + """Service handle set_dev_value services.""" + address = service.data.get(ATTR_ADDRESS) + channel = service.data.get(ATTR_CHANNEL) + param = service.data.get(ATTR_PARAM) + value = service.data.get(ATTR_VALUE) + + # device not found + hmdevice = _device_from_servicecall(hass, service) + if hmdevice is None: + _LOGGER.error("%s not found!", address) + return + + # call key + hmdevice.setValue(param, value, channel) + + hass.services.register( + DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device, + descriptions[DOMAIN][SERVICE_SET_DEV_VALUE], + schema=SCHEMA_SERVICE_SET_DEV_VALUE) return True @@ -274,22 +371,36 @@ def setup(hass, config): def _system_callback_handler(hass, config, src, *args): """Callback handler.""" if src == 'newDevices': - _LOGGER.debug("newDevices with: %s", str(args)) + _LOGGER.debug("newDevices with: %s", args) # pylint: disable=unused-variable (interface_id, dev_descriptions) = args - key_dict = {} + proxy = interface_id.split('-')[-1] + + # device support active? + if not hass.data[DATA_DEVINIT][proxy]: + return + + ## # Get list of all keys of the devices (ignoring channels) + key_dict = {} for dev in dev_descriptions: key_dict[dev['ADDRESS'].split(':')[0]] = True + ## + # remove device they allready init by HA + tmp_devs = key_dict.copy() + for dev in tmp_devs: + if dev in hass.data[DATA_STORE]: + del key_dict[dev] + else: + hass.data[DATA_STORE].append(dev) + # Register EVENTS # Search all device with a EVENTNODE that include data - bound_event_callback = partial(_hm_event_handler, hass) + bound_event_callback = partial(_hm_event_handler, hass, proxy) for dev in key_dict: - if dev not in HOMEMATIC.devices: - continue + hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev) - hmdevice = HOMEMATIC.devices.get(dev) # have events? if len(hmdevice.EVENTNODE) > 0: _LOGGER.debug("Register Events from %s", dev) @@ -307,7 +418,8 @@ def _system_callback_handler(hass, config, src, *args): ('sensor', DISCOVER_SENSORS), ('climate', DISCOVER_CLIMATE)): # Get all devices of a specific type - found_devices = _get_devices(discovery_type, key_dict) + found_devices = _get_devices( + hass, discovery_type, key_dict, proxy) # When devices of this type are found # they are setup in HA and an event is fired @@ -318,12 +430,12 @@ def _system_callback_handler(hass, config, src, *args): }, config) -def _get_devices(device_type, keys): +def _get_devices(hass, device_type, keys, proxy): """Get the Homematic devices.""" device_arr = [] for key in keys: - device = HOMEMATIC.devices[key] + device = hass.data[DATA_HOMEMATIC].devices[proxy][key] class_name = device.__class__.__name__ metadata = {} @@ -357,6 +469,7 @@ def _get_devices(device_type, keys): device_dict = { CONF_PLATFORM: "homematic", ATTR_ADDRESS: key, + ATTR_PROXY: proxy, ATTR_NAME: name, ATTR_CHANNEL: channel } @@ -395,28 +508,29 @@ def _create_ha_name(name, channel, param, count): return "{} {} {}".format(name, channel, param) -def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, +def setup_hmdevice_discovery_helper(hass, hmdevicetype, discovery_info, add_callback_devices): """Helper to setup Homematic devices with discovery info.""" + devices = [] for config in discovery_info[ATTR_DISCOVER_DEVICES]: _LOGGER.debug("Add device %s from config: %s", str(hmdevicetype), str(config)) # create object and add to HA - new_device = hmdevicetype(config) + new_device = hmdevicetype(hass, config) new_device.link_homematic() + devices.append(new_device) - add_callback_devices([new_device]) - + add_callback_devices(devices) return True -def _hm_event_handler(hass, device, caller, attribute, value): +def _hm_event_handler(hass, proxy, device, caller, attribute, value): """Handle all pyhomematic device events.""" try: channel = int(device.split(":")[1]) address = device.split(":")[0] - hmdevice = HOMEMATIC.devices.get(address) + hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address) except (TypeError, ValueError): _LOGGER.error("Event handling channel convert error!") return @@ -448,46 +562,40 @@ def _hm_event_handler(hass, device, caller, attribute, value): _LOGGER.warning("Event is unknown and not forwarded to HA") -def _hm_service_virtualkey(call): - """Callback for handle virtualkey services.""" - address = call.data.get(ATTR_ADDRESS) - channel = call.data.get(ATTR_CHANNEL) - param = call.data.get(ATTR_PARAM) +def _device_from_servicecall(hass, service): + """Extract homematic device from service call.""" + address = service.data.get(ATTR_ADDRESS) + proxy = service.data.get(ATTR_PROXY) - if address not in HOMEMATIC.devices: - _LOGGER.error("%s not found for service virtualkey!", address) - return - hmdevice = HOMEMATIC.devices.get(address) + if proxy: + return hass.data[DATA_HOMEMATIC].devices[proxy].get(address) - # if param exists for this device - if hmdevice is None or param not in hmdevice.ACTIONNODE: - _LOGGER.error("%s not datapoint in hm device %s", param, address) - return - - # channel exists? - if channel in hmdevice.ACTIONNODE[param]: - _LOGGER.error("%i is not a channel in hm device %s", channel, address) - return - - # call key - hmdevice.actionNodeData(param, 1, channel) + for _, devices in hass.data[DATA_HOMEMATIC].devices.items(): + if address in devices: + return devices[address] class HMHub(Entity): """The Homematic hub. I.e. CCU2/HomeGear.""" - def __init__(self, variables_store, use_variables=False): + def __init__(self, hass, component, name, use_variables): """Initialize Homematic hub.""" + self.hass = hass + self._homematic = hass.data[DATA_HOMEMATIC] + self._component = component + self._name = name self._state = STATE_UNKNOWN - self._store = variables_store + self._store = {} self._use_variables = use_variables - self.update() + # load data + self._update_hub_state() + self._init_variables() @property def name(self): """Return the name of the device.""" - return 'Homematic' + return self._name @property def state(self): @@ -504,11 +612,6 @@ class HMHub(Entity): """Return the icon to use in the frontend, if any.""" return "mdi:gradient" - @property - def available(self): - """Return true if device is available.""" - return True if HOMEMATIC is not None else False - def update(self): """Update Hub data and all HM variables.""" self._update_hub_state() @@ -517,30 +620,48 @@ class HMHub(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATE_HUB) def _update_hub_state(self): """Retrieve latest state.""" - if HOMEMATIC is None: - return - state = HOMEMATIC.getServiceMessages() + state = self._homematic.getServiceMessages(self._name) self._state = STATE_UNKNOWN if state is None else len(state) @Throttle(MIN_TIME_BETWEEN_UPDATE_VAR) def _update_variables_state(self): """Retrive all variable data and update hmvariable states.""" - if HOMEMATIC is None or not self._use_variables: + if not self._use_variables: return - variables = HOMEMATIC.getAllSystemVariables() - if variables is not None: - for key, value in variables.items(): - if key in self._store: - self._store.get(key).hm_update(value) + + variables = self._homematic.getAllSystemVariables(self._name) + if variables is None: + return + + for key, value in variables.items(): + if key in self._store: + self._store.get(key).hm_update(value) + + def _init_variables(self): + """Load variables from hub.""" + if not self._use_variables: + return + + variables = self._homematic.getAllSystemVariables(self._name) + if variables is None: + return + + entities = [] + for key, value in variables.items(): + entities.append(HMVariable(self.hass, self._name, key, value)) + self._component.add_entities(entities) class HMVariable(Entity): """The Homematic system variable.""" - def __init__(self, name, state): + def __init__(self, hass, hub_name, name, state): """Initialize Homematic hub.""" + self.hass = hass + self._homematic = hass.data[DATA_HOMEMATIC] self._state = state self._name = name + self._hub_name = hub_name @property def name(self): @@ -562,31 +683,41 @@ class HMVariable(Entity): """Return false. Homematic Hub object update variable.""" return False + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = { + 'hub': self._hub_name, + } + return attr + def hm_update(self, value): """Update variable over Hub object.""" if value != self._state: self._state = value - self.update_ha_state() + self.schedule_update_ha_state() def hm_set(self, value): """Set variable on homematic controller.""" - if HOMEMATIC is not None: - if isinstance(self._state, bool): - value = cv.boolean(value) - else: - value = float(value) - HOMEMATIC.setSystemVariable(self._name, value) - self._state = value - self.update_ha_state() + if isinstance(self._state, bool): + value = cv.boolean(value) + else: + value = float(value) + self._homematic.setSystemVariable(self._hub_name, self._name, value) + self._state = value + self.schedule_update_ha_state() class HMDevice(Entity): """The Homematic device base object.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize a generic Homematic device.""" + self.hass = hass + self._homematic = hass.data[DATA_HOMEMATIC] self._name = config.get(ATTR_NAME) self._address = config.get(ATTR_ADDRESS) + self._proxy = config.get(ATTR_PROXY) self._channel = config.get(ATTR_CHANNEL) self._state = config.get(ATTR_PARAM) self._data = {} @@ -636,6 +767,7 @@ class HMDevice(Entity): # static attributes attr['ID'] = self._hmdevice.ADDRESS + attr['proxy'] = self._proxy return attr @@ -645,39 +777,31 @@ class HMDevice(Entity): if self._connected: return True - # pyhomematic is loaded - if HOMEMATIC is None: - return False + # Init + self._hmdevice = self._homematic.devices[self._proxy][self._address] + self._connected = True - # Does a HMDevice from pyhomematic exist? - if self._address in HOMEMATIC.devices: - # Init - self._hmdevice = HOMEMATIC.devices[self._address] - self._connected = True + # Check if Homematic class is okay for HA class + _LOGGER.info("Start linking %s to %s", self._address, self._name) + try: + # Init datapoints of this object + self._init_data() + if self.hass.data[DATA_DELAY]: + # We delay / pause loading of data to avoid overloading + # of CCU / Homegear when doing auto detection + time.sleep(self.hass.data[DATA_DELAY]) + self._load_data_from_hm() + _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) - # Check if Homematic class is okay for HA class - _LOGGER.info("Start linking %s to %s", self._address, self._name) - try: - # Init datapoints of this object - self._init_data() - if HOMEMATIC_LINK_DELAY: - # We delay / pause loading of data to avoid overloading - # of CCU / Homegear when doing auto detection - time.sleep(HOMEMATIC_LINK_DELAY) - self._load_data_from_hm() - _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) - - # Link events from pyhomatic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH - _LOGGER.debug("%s linking done", self._name) - # pylint: disable=broad-except - except Exception as err: - self._connected = False - _LOGGER.error("Exception while linking %s: %s", - self._address, str(err)) - else: - _LOGGER.debug("%s not found in HOMEMATIC.devices", self._address) + # Link events from pyhomatic + self._subscribe_homematic_events() + self._available = not self._hmdevice.UNREACH + _LOGGER.debug("%s linking done", self._name) + # pylint: disable=broad-except + except Exception as err: + self._connected = False + _LOGGER.error("Exception while linking %s: %s", + self._address, str(err)) def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 299791939c8..27653ba2233 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -7,8 +7,9 @@ https://home-assistant.io/components/light.homematic/ import logging from homeassistant.components.light import (ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.homematic import HMDevice from homeassistant.const import STATE_UNKNOWN -import homeassistant.components.homematic as homematic +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -22,14 +23,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return + homematic = get_component("homematic") return homematic.setup_hmdevice_discovery_helper( + hass, HMLight, discovery_info, add_devices ) -class HMLight(homematic.HMDevice, Light): +class HMLight(HMDevice, Light): """Representation of a Homematic light.""" @property diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index fc907ae76b7..e252e2d30c6 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -10,7 +10,8 @@ properly configured. import logging from homeassistant.const import STATE_UNKNOWN -import homeassistant.components.homematic as homematic +from homeassistant.components.homematic import HMDevice +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -48,14 +49,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return + homematic = get_component("homematic") return homematic.setup_hmdevice_discovery_helper( + hass, HMSensor, discovery_info, add_callback_devices ) -class HMSensor(homematic.HMDevice): +class HMSensor(HMDevice): """Represents various Homematic sensors in Home Assistant.""" @property diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index fb114f27dd3..46a3a46ced6 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -90,7 +90,11 @@ homematic: description: Event to send i.e. PRESS_LONG, PRESS_SHORT example: PRESS_LONG - set_value: + proxy: + description: (Optional) for set a hosts value + example: Hosts name from config + + set_var_value: description: Set the name of a node. fields: @@ -102,6 +106,33 @@ homematic: description: New value example: 1 + set_dev_value: + description: Set a device property on RPC XML inteface. + + fields: + address: + description: Address of homematic device or BidCoS-RF for virtual remote + example: BidCoS-RF + + channel: + description: Channel for calling a keypress + example: 1 + + param: + description: Event to send i.e. PRESS_LONG, PRESS_SHORT + example: PRESS_LONG + + proxy: + description: (Optional) for set a hosts value + example: Hosts name from config + + value: + description: New value + example: 1 + + reconnect: + description: Reconnect to all Homematic Hubs. + openalpr: scan: description: Scan immediately a device. diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index e13027780c6..793843ec214 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -6,8 +6,9 @@ https://home-assistant.io/components/switch.homematic/ """ import logging from homeassistant.components.switch import SwitchDevice +from homeassistant.components.homematic import HMDevice from homeassistant.const import STATE_UNKNOWN -import homeassistant.components.homematic as homematic +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -19,14 +20,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return + homematic = get_component("homematic") return homematic.setup_hmdevice_discovery_helper( + hass, HMSwitch, discovery_info, add_callback_devices ) -class HMSwitch(homematic.HMDevice, SwitchDevice): +class HMSwitch(HMDevice, SwitchDevice): """Representation of a Homematic switch.""" @property diff --git a/requirements_all.txt b/requirements_all.txt index cfddeefb83b..88cab503659 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -390,7 +390,7 @@ pyenvisalink==1.9 pyfttt==0.3 # homeassistant.components.homematic -pyhomematic==0.1.16 +pyhomematic==0.1.18 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 From 86388f5af26a70434fec7d54aba6779c387c9801 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 30 Nov 2016 14:21:00 +0100 Subject: [PATCH 096/137] Upgrade Sphinx to 1.4.9 (#4641) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 85405fb6eab..b39476859f2 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.4.8 +Sphinx==1.4.9 sphinx-autodoc-typehints==1.1.0 sphinx-autodoc-annotation==1.0.post1 From 71da9d2f50e80da1cc0a9545c6d7a5b815042879 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 30 Nov 2016 22:02:18 +0100 Subject: [PATCH 097/137] Fix mysensors ir switch overwriting devices (#4612) --- homeassistant/components/switch/mysensors.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index d6b348d8274..44bbfcb16d9 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -38,6 +38,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if not gateways: return + platform_devices = [] + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. @@ -88,6 +90,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( map_sv_types, devices, add_devices, device_class_map)) + platform_devices.append(devices) def send_ir_code_service(service): """Set IR code as device state attribute.""" @@ -95,11 +98,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ir_code = service.data.get(ATTR_IR_CODE) if entity_ids: - _devices = [device for device in devices.values() + _devices = [device for gw_devs in platform_devices + for device in gw_devs.values() if isinstance(device, MySensorsIRSwitch) and device.entity_id in entity_ids] else: - _devices = [device for device in devices.values() + _devices = [device for gw_devs in platform_devices + for device in gw_devs.values() if isinstance(device, MySensorsIRSwitch)] kwargs = {ATTR_IR_CODE: ir_code} From b35fa4f1c10fb663f6dcf1a57a3df691b853f21b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Nov 2016 13:02:45 -0800 Subject: [PATCH 098/137] Finish all tasks before setup phase is done (#4606) --- homeassistant/bootstrap.py | 3 ++ homeassistant/core.py | 6 ++++ tests/helpers/test_discovery.py | 3 +- tests/test_bootstrap.py | 55 ++++++++++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 59061f40754..95bb7cee24a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -365,6 +365,7 @@ def async_from_config_dict(config: Dict[str, Any], Dynamically loads required components and its dependencies. This method is a coroutine. """ + hass.async_track_tasks() setup_lock = hass.data.get('setup_lock') if setup_lock is None: setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop) @@ -427,6 +428,8 @@ def async_from_config_dict(config: Dict[str, Any], setup_lock.release() + yield from hass.async_stop_track_tasks() + return hass diff --git a/homeassistant/core.py b/homeassistant/core.py index f358903735b..86cfec7099c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -236,6 +236,12 @@ class HomeAssistant(object): """Track tasks so you can wait for all tasks to be done.""" self.async_add_job = self._async_add_job_tracking + @asyncio.coroutine + def async_stop_track_tasks(self): + """Track tasks so you can wait for all tasks to be done.""" + yield from self.async_block_till_done() + self.async_add_job = self._async_add_job + @callback def async_run_job(self, target: Callable[..., None], *args: Any) -> None: """Run a job from within the event loop. diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index c062cb7b566..5d6faf2f7c4 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -166,7 +166,8 @@ class TestHelpersDiscovery: def component1_setup(hass, config): """Setup mock component.""" - discovery.discover(hass, 'test_component2') + discovery.discover(hass, 'test_component2', + component='test_component2') return True def component2_setup(hass, config): diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index def2cbb68d4..dd3b09d932f 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,20 +1,26 @@ """Test the bootstrapping.""" # pylint: disable=protected-access +import os from unittest import mock import threading import logging import voluptuous as vol +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_START +import homeassistant.config as config_util from homeassistant import bootstrap, loader import homeassistant.util.dt as dt_util from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers import discovery from tests.common import \ get_test_home_assistant, MockModule, MockPlatform, \ - assert_setup_component, patch_yaml_files + assert_setup_component, patch_yaml_files, get_test_config_dir ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE +VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) _LOGGER = logging.getLogger(__name__) @@ -43,6 +49,8 @@ class TestBootstrap: dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE self.hass.stop() loader._COMPONENT_CACHE = self.backup_cache + if os.path.isfile(VERSION_PATH): + os.remove(VERSION_PATH) @mock.patch( # prevent .HA_VERISON file from being written @@ -381,3 +389,48 @@ class TestBootstrap: assert bootstrap.setup_component(self.hass, 'disabled_component') assert loader.get_component('disabled_component') is not None assert 'disabled_component' in self.hass.config.components + + def test_all_work_done_before_start(self): + """Test all init work done till start.""" + call_order = [] + + def component1_setup(hass, config): + """Setup mock component.""" + discovery.discover(hass, 'test_component2', + component='test_component2') + discovery.discover(hass, 'test_component3', + component='test_component3') + return True + + def component_track_setup(hass, config): + """Setup mock component.""" + call_order.append(1) + return True + + loader.set_component( + 'test_component1', + MockModule('test_component1', setup=component1_setup)) + + loader.set_component( + 'test_component2', + MockModule('test_component2', setup=component_track_setup)) + + loader.set_component( + 'test_component3', + MockModule('test_component3', setup=component_track_setup)) + + @callback + def track_start(event): + """Track start event.""" + call_order.append(2) + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, track_start) + + self.hass.loop.run_until_complete = \ + lambda _: self.hass.block_till_done() + + bootstrap.from_config_dict({'test_component1': None}, self.hass) + + self.hass.start() + + assert call_order == [1, 1, 2] From b1ef5042f91b4f431e6967cd726c9d79b88353dc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Nov 2016 13:03:09 -0800 Subject: [PATCH 099/137] Make updater more robust (#4625) --- homeassistant/components/updater.py | 29 +++++++++++++++++++++-------- tests/components/test_updater.py | 13 ++++++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index a4203876348..c05aedbb888 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -41,6 +41,11 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: { vol.Optional(CONF_REPORTING, default=True): cv.boolean }}, extra=vol.ALLOW_EXTRA) +RESPONSE_SCHEMA = vol.Schema({ + vol.Required('version'): str, + vol.Required('release-notes'): cv.url, +}) + def _create_uuid(hass, filename=UPDATER_UUID_FILE): """Create UUID and save it in a file.""" @@ -83,7 +88,12 @@ def setup(hass, config): def check_newest_version(hass, huuid): """Check if a new version is available and report if one is.""" - newest, releasenotes = get_newest_version(huuid) + result = get_newest_version(huuid) + + if result is None: + return + + newest, releasenotes = result if newest is None or 'dev' in CURRENT_VERSION: return @@ -129,21 +139,24 @@ def get_newest_version(huuid): if not huuid: info_object = {} + res = None try: req = requests.post(UPDATER_URL, json=info_object, timeout=5) res = req.json() + res = RESPONSE_SCHEMA(res) + _LOGGER.info(("Submitted analytics to Home Assistant servers. " "Information submitted includes %s"), info_object) return (res['version'], res['release-notes']) except requests.RequestException: - _LOGGER.exception("Could not contact Home Assistant Update to check" - "for updates") + _LOGGER.error("Could not contact Home Assistant Update to check " + "for updates") return None + except ValueError: - _LOGGER.exception("Received invalid response from Home Assistant" - "Update") + _LOGGER.error("Received invalid response from Home Assistant Update") return None - except KeyError: - _LOGGER.exception("Response from Home Assistant Update did not" - "include version") + + except vol.Invalid: + _LOGGER.error('Got unexpected response: %s', res) return None diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py index 94a43c1f281..8ca136bd8d7 100644 --- a/tests/components/test_updater.py +++ b/tests/components/test_updater.py @@ -6,6 +6,7 @@ import os import requests import requests_mock +import voluptuous as vol from homeassistant.bootstrap import setup_component from homeassistant.components import updater @@ -86,7 +87,7 @@ class TestUpdater(unittest.TestCase): mock_get.side_effect = ValueError self.assertIsNone(updater.get_newest_version(uuid)) - mock_get.side_effect = KeyError + mock_get.side_effect = vol.Invalid('Expected dictionary') self.assertIsNone(updater.get_newest_version(uuid)) def test_uuid_function(self): @@ -119,3 +120,13 @@ class TestUpdater(unittest.TestCase): assert len(history) == 1 assert history[0].json() == {} + + @patch('homeassistant.components.updater.get_newest_version') + def test_error_during_fetch_works( + self, mock_get_newest_version): + """Test if no entity is created if same version.""" + mock_get_newest_version.return_value = None + + updater.check_newest_version(self.hass, None) + + self.assertIsNone(self.hass.states.get(updater.ENTITY_ID)) From e5504b39ecbe61378732cd0150cac9e0d88e6da1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Nov 2016 13:05:58 -0800 Subject: [PATCH 100/137] Close aiohttp responses (#4624) * Close aiohttp responses * Update generic.py --- homeassistant/components/camera/generic.py | 10 ++-- homeassistant/components/camera/mjpeg.py | 27 +++++----- homeassistant/components/camera/synology.py | 54 +++++++++++-------- .../components/media_player/__init__.py | 35 +++++++----- homeassistant/components/sensor/yr.py | 7 ++- homeassistant/components/switch/hook.py | 21 ++++++-- 6 files changed, 98 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index a73132282bf..74b4161f438 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -97,8 +97,7 @@ class GenericCamera(Camera): def fetch(): """Read image from a URL.""" try: - kwargs = {'timeout': 10, 'auth': self._auth} - response = requests.get(url, **kwargs) + response = requests.get(url, timeout=10, auth=self._auth) return response.content except requests.exceptions.RequestException as error: _LOGGER.error('Error getting camera image: %s', error) @@ -108,13 +107,13 @@ class GenericCamera(Camera): None, fetch) # async else: + response = None try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): response = yield from websession.get( url, auth=self._auth) - self._last_image = yield from response.read() - yield from response.release() + self._last_image = yield from response.read() except asyncio.TimeoutError: _LOGGER.error('Timeout getting camera image') return self._last_image @@ -122,6 +121,9 @@ class GenericCamera(Camera): aiohttp.errors.ClientDisconnectedError) as err: _LOGGER.error('Error getting new camera image: %s', err) return self._last_image + finally: + if response is not None: + self.hass.async_add_job(response.release()) self._last_url = url return self._last_image diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index d96ea4ab0a3..981ed9dbf49 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -103,29 +103,32 @@ class MjpegCamera(Camera): # connect to stream websession = async_get_clientsession(self.hass) + stream = None + response = None try: with async_timeout.timeout(10, loop=self.hass.loop): - stream = yield from websession.get( - self._mjpeg_url, - auth=self._auth - ) - except asyncio.TimeoutError: - raise HTTPGatewayTimeout() + stream = yield from websession.get(self._mjpeg_url, + auth=self._auth) - response = web.StreamResponse() - response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - yield from response.prepare(request) + yield from response.prepare(request) - try: while True: data = yield from stream.content.read(102400) if not data: break response.write(data) + + except asyncio.TimeoutError: + raise HTTPGatewayTimeout() + finally: - self.hass.async_add_job(stream.release()) - yield from response.write_eof() + if stream is not None: + self.hass.async_add_job(stream.release()) + if response is not None: + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 1db83ddf762..6d5b4546933 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -73,24 +73,27 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 'version': '1', 'query': 'SYNO.' } + query_req = None try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): query_req = yield from websession_init.get( syno_api_url, params=query_payload ) + + query_resp = yield from query_req.json() + auth_path = query_resp['data'][AUTH_API]['path'] + camera_api = query_resp['data'][CAMERA_API]['path'] + camera_path = query_resp['data'][CAMERA_API]['path'] + streaming_path = query_resp['data'][STREAMING_API]['path'] + except (asyncio.TimeoutError, aiohttp.errors.ClientError): _LOGGER.exception("Error on %s", syno_api_url) return False - query_resp = yield from query_req.json() - auth_path = query_resp['data'][AUTH_API]['path'] - camera_api = query_resp['data'][CAMERA_API]['path'] - camera_path = query_resp['data'][CAMERA_API]['path'] - streaming_path = query_resp['data'][STREAMING_API]['path'] - - # cleanup - yield from query_req.release() + finally: + if query_req is not None: + yield from query_req.release() # Authticate to NAS to get a session id syno_auth_url = SYNO_API_URL.format( @@ -166,20 +169,23 @@ def get_session_id(hass, websession, username, password, login_url): 'session': 'SurveillanceStation', 'format': 'sid' } + auth_req = None try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): auth_req = yield from websession.get( login_url, params=auth_payload ) + auth_resp = yield from auth_req.json() + return auth_resp['data']['sid'] + except (asyncio.TimeoutError, aiohttp.errors.ClientError): _LOGGER.exception("Error on %s", login_url) return False - auth_resp = yield from auth_req.json() - yield from auth_req.release() - - return auth_resp['data']['sid'] + finally: + if auth_req is not None: + yield from auth_req.release() class SynologyCamera(Camera): @@ -247,30 +253,34 @@ class SynologyCamera(Camera): 'cameraId': self._camera_id, 'format': 'mjpeg' } + stream = None + response = None try: with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): stream = yield from self._websession.get( streaming_url, params=streaming_payload ) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): - _LOGGER.exception("Error on %s", streaming_url) - raise HTTPGatewayTimeout() + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - response = web.StreamResponse() - response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + yield from response.prepare(request) - yield from response.prepare(request) - - try: while True: data = yield from stream.content.read(102400) if not data: break response.write(data) + + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", streaming_url) + raise HTTPGatewayTimeout() + finally: - self.hass.async_add_job(stream.release()) - yield from response.write_eof() + if stream is not None: + self.hass.async_add_job(stream.release()) + if response is not None: + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d0cacd47b75..fa2ecee4337 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -722,28 +722,35 @@ def _async_fetch_image(hass, url): return cache_images[url] content, content_type = (None, None) + websession = async_get_clientsession(hass) + response = None try: - websession = async_get_clientsession(hass) with async_timeout.timeout(10, loop=hass.loop): response = yield from websession.get(url) - if response.status == 200: - content = yield from response.read() - content_type = response.headers.get(CONTENT_TYPE_HEADER) - yield from response.release() + if response.status == 200: + content = yield from response.read() + content_type = response.headers.get(CONTENT_TYPE_HEADER) + except asyncio.TimeoutError: pass - if content: - cache_images[url] = (content, content_type) - cache_urls.append(url) + finally: + if response is not None: + yield from response.release() - while len(cache_urls) > cache_maxsize: - # remove oldest item from cache - oldest_url = cache_urls[0] - if oldest_url in cache_images: - del cache_images[oldest_url] + if not content: + return (None, None) - cache_urls = cache_urls[1:] + cache_images[url] = (content, content_type) + cache_urls.append(url) + + while len(cache_urls) > cache_maxsize: + # remove oldest item from cache + oldest_url = cache_urls[0] + if oldest_url in cache_images: + del cache_images[oldest_url] + + cache_urls = cache_urls[1:] return content, content_type diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index e3cc5186230..4eeb809b7cf 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -155,6 +155,7 @@ class YrData(object): nxt) if self._nextrun is None or dt_util.utcnow() >= self._nextrun: + resp = None try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): @@ -163,12 +164,16 @@ class YrData(object): try_again('{} returned {}'.format(self._url, resp.status)) return text = yield from resp.text() - self.hass.async_add_job(resp.release()) + except (asyncio.TimeoutError, aiohttp.errors.ClientError, aiohttp.errors.ClientDisconnectedError) as err: try_again(err) return + finally: + if resp is not None: + self.hass.async_add_job(resp.release()) + try: import xmltodict self.data = xmltodict.parse(text)['weatherdata'] diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 29fe8372fab..eba64c6aeb1 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -34,6 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) websession = async_get_clientsession(hass) + response = None try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): response = yield from websession.post( @@ -41,12 +42,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): data={ 'username': username, 'password': password}) - data = yield from response.json() + data = yield from response.json() except (asyncio.TimeoutError, aiohttp.errors.ClientError, aiohttp.errors.ClientDisconnectedError) as error: _LOGGER.error("Failed authentication API call: %s", error) return False + finally: + if response is not None: + yield from response.close() try: token = data['data']['token'] @@ -54,17 +58,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No token. Check username and password") return False + response = None try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): response = yield from websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": data['data']['token']}) - data = yield from response.json() + data = yield from response.json() except (asyncio.TimeoutError, aiohttp.errors.ClientError, aiohttp.errors.ClientDisconnectedError) as error: _LOGGER.error("Failed getting devices: %s", error) return False + finally: + if response is not None: + yield from response.close() yield from async_add_devices( HookSmartHome( @@ -102,18 +110,25 @@ class HookSmartHome(SwitchDevice): @asyncio.coroutine def _send(self, url): """Send the url to the Hook API.""" + response = None try: _LOGGER.debug("Sending: %s", url) websession = async_get_clientsession(self.hass) with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): response = yield from websession.get( url, params={"token": self._token}) - data = yield from response.json() + data = yield from response.json() + except (asyncio.TimeoutError, aiohttp.errors.ClientError, aiohttp.errors.ClientDisconnectedError) as error: _LOGGER.error("Failed setting state: %s", error) return False + + finally: + if response is not None: + yield from response.close() + _LOGGER.debug("Got: %s", data) return data['return_value'] == '1' From 9c6609cb796afc71c72bec5c3674026a420e4560 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Wed, 30 Nov 2016 16:07:17 -0500 Subject: [PATCH 101/137] Added support to Amcrest camera (#4573) * Introduced support to Amcrest IP Cameras * Fixed lint issues * Fixed requirements test * * Implemented test to verify crendentials during camera setup * Added persistent_notification in case of error when during Amcrest setup --- .coveragerc | 1 + homeassistant/components/camera/amcrest.py | 79 ++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 83 insertions(+) create mode 100644 homeassistant/components/camera/amcrest.py diff --git a/.coveragerc b/.coveragerc index c7d1e4237f5..8e01d88154f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -124,6 +124,7 @@ omit = homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py + homeassistant/components/camera/amcrest.py homeassistant/components/camera/bloomsky.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py new file mode 100644 index 00000000000..ff862f2db11 --- /dev/null +++ b/homeassistant/components/camera/amcrest.py @@ -0,0 +1,79 @@ +""" +This component provides basic support for Amcrest IP cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.amcrest/ +""" +import logging +import voluptuous as vol + +from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) +from homeassistant.helpers import config_validation as cv +import homeassistant.loader as loader + +REQUIREMENTS = ['amcrest==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 80 +DEFAULT_NAME = 'Amcrest Camera' + +NOTIFICATION_ID = 'amcrest_notification' +NOTIFICATION_TITLE = 'Amcrest Camera Setup' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup an Amcrest IP Camera.""" + from amcrest import AmcrestCamera + data = AmcrestCamera(config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) + + persistent_notification = loader.get_component('persistent_notification') + try: + data.camera.current_time + # pylint: disable=broad-except + except Exception as ex: + _LOGGER.error('Unable to connect to Amcrest camera: %s', str(ex)) + persistent_notification.create( + hass, 'Error: {}
    ' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + add_devices([AmcrestCam(config, data)]) + return True + + +class AmcrestCam(Camera): + """An implementation of an Amcrest IP camera.""" + + def __init__(self, device_info, data): + """Initialize an Amcrest camera.""" + super(AmcrestCam, self).__init__() + self._name = device_info.get(CONF_NAME) + self._data = data + + def camera_image(self): + """Return a still image reponse from the camera.""" + # Send the request to snap a picture and return raw jpg data + response = self._data.camera.snapshot() + return response.data + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/requirements_all.txt b/requirements_all.txt index 88cab503659..ad1a41b457b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -33,6 +33,9 @@ TwitterAPI==2.4.2 # homeassistant.components.http aiohttp_cors==0.5.0 +# homeassistant.components.camera.amcrest +amcrest==1.0.0 + # homeassistant.components.apcupsd apcaccess==0.0.4 From 406afbb369cd0e2f33654746a446d5b3e4120f0e Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Wed, 30 Nov 2016 22:07:57 +0100 Subject: [PATCH 102/137] Philips controls (#4441) * Add channel switching for philips tvs. * Disable track buttons when not watching tv. * Undo isort config. * Yes it does. * Just testing some assumption on hound's flake8 behaviour. * Revert "Just testing some assumption on hound's flake8 behaviour." This reverts commit ff9940b39e2c68785287c8567bf4862a4a49fe78. * poke --- .../components/media_player/philips_js.py | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 02e520bb549..b7665b199a4 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -9,13 +9,14 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, MediaPlayerDevice) -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_HOST, CONF_NAME) -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +from homeassistant.util import Throttle REQUIREMENTS = ['ha-philipsjs==0.0.1'] @@ -26,6 +27,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE +SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ + SUPPORT_PREVIOUS_TRACK + DEFAULT_DEVICE = 'default' DEFAULT_HOST = '127.0.0.1' DEFAULT_NAME = 'Philips TV' @@ -68,6 +72,8 @@ class PhilipsTV(MediaPlayerDevice): self._source_list = [] self._connfail = 0 self._source_mapping = {} + self._watching_tv = None + self._channel_name = None @property def name(self): @@ -82,7 +88,10 @@ class PhilipsTV(MediaPlayerDevice): @property def supported_media_commands(self): """Flag of media commands that are supported.""" - return SUPPORT_PHILIPS_JS + if self._watching_tv: + return SUPPORT_PHILIPS_JS_TV + else: + return SUPPORT_PHILIPS_JS @property def state(self): @@ -106,6 +115,7 @@ class PhilipsTV(MediaPlayerDevice): self._source = source if not self._tv.on: self._state = STATE_OFF + self._watching_tv = bool(self._tv.source_id == 'tv') @property def volume_level(self): @@ -141,10 +151,24 @@ class PhilipsTV(MediaPlayerDevice): if not self._tv.on: self._state = STATE_OFF + def media_previous_track(self): + """Send rewind commmand.""" + self._tv.sendKey('Previous') + + def media_next_track(self): + """Send fast forward commmand.""" + self._tv.sendKey('Next') + @property def media_title(self): """Title of current playing media.""" - return self._source + if self._watching_tv: + if self._channel_name: + return '{} - {}'.format(self._source, self._channel_name) + else: + return self._source + else: + return self._source @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -167,3 +191,12 @@ class PhilipsTV(MediaPlayerDevice): self._state = STATE_ON else: self._state = STATE_OFF + + self._watching_tv = bool(self._tv.source_id == 'tv') + + self._tv.getChannelId() + self._tv.getChannels() + if self._tv.channels and self._tv.channel_id in self._tv.channels: + self._channel_name = self._tv.channels[self._tv.channel_id]['name'] + else: + self._channel_name = None From 4c03d670c1443bc2d0d6bed6678e4eff15960029 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 30 Nov 2016 16:12:26 -0500 Subject: [PATCH 103/137] Wink PubNub v4 (#4561) * PubNub v4 * Updated to pubnubsub-handler 0.0.5 * Updated requirements_all.txt --- .../components/binary_sensor/wink.py | 25 ++---- homeassistant/components/climate/wink.py | 6 +- homeassistant/components/cover/wink.py | 8 +- homeassistant/components/light/wink.py | 6 +- homeassistant/components/lock/wink.py | 6 +- homeassistant/components/sensor/wink.py | 17 +++-- homeassistant/components/switch/wink.py | 14 ++-- homeassistant/components/wink.py | 76 +++++++++++-------- requirements_all.txt | 4 +- 9 files changed, 82 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index e4448d96e36..2d0e3f7226f 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -4,8 +4,6 @@ Support for Wink binary sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ -import json -import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.sensor.wink import WinkDevice @@ -34,38 +32,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for sensor in pywink.get_sensors(): if sensor.capability() in SENSOR_TYPES: - add_devices([WinkBinarySensorDevice(sensor)]) + add_devices([WinkBinarySensorDevice(sensor, hass)]) for key in pywink.get_keys(): - add_devices([WinkBinarySensorDevice(key)]) + add_devices([WinkBinarySensorDevice(key, hass)]) for sensor in pywink.get_smoke_and_co_detectors(): - add_devices([WinkBinarySensorDevice(sensor)]) + add_devices([WinkBinarySensorDevice(sensor, hass)]) class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): """Representation of a Wink binary sensor.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the Wink binary sensor.""" - super().__init__(wink) + super().__init__(wink, hass) wink = get_component('wink') self._unit_of_measurement = self.wink.UNIT self.capability = self.wink.capability() - def _pubnub_update(self, message, channel): - try: - if 'data' in message: - json_data = json.dumps(message.get('data')) - else: - json_data = message - self.wink.pubnub_update(json.loads(json_data)) - self.update_ha_state() - except (AttributeError, KeyError): - error = "Pubnub returned invalid json for " + self.name - logging.getLogger(__name__).error(error) - self.update_ha_state(True) - @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index a0094a7c290..733d2baddf7 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink thermostat.""" import pywink temp_unit = hass.config.units.temperature_unit - add_devices(WinkThermostat(thermostat, temp_unit) + add_devices(WinkThermostat(thermostat, hass, temp_unit) for thermostat in pywink.get_thermostats()) @@ -38,9 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkThermostat(WinkDevice, ClimateDevice): """Representation of a Wink thermostat.""" - def __init__(self, wink, temp_unit): + def __init__(self, wink, hass, temp_unit): """Initialize the Wink device.""" - super().__init__(wink) + super().__init__(wink, hass) wink = get_component('wink') self._config_temp_unit = temp_unit diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index c57c2180446..264cec70a7e 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -15,18 +15,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink cover platform.""" import pywink - add_devices(WinkCoverDevice(shade) for shade in + add_devices(WinkCoverDevice(shade, hass) for shade in pywink.get_shades()) - add_devices(WinkCoverDevice(door) for door in + add_devices(WinkCoverDevice(door, hass) for door in pywink.get_garage_doors()) class WinkCoverDevice(WinkDevice, CoverDevice): """Representation of a Wink cover device.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the cover.""" - WinkDevice.__init__(self, wink) + WinkDevice.__init__(self, wink, hass) def close_cover(self): """Close the shade.""" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 1d292a53419..1a4556ee46b 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -23,15 +23,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink lights.""" import pywink - add_devices(WinkLight(light) for light in pywink.get_bulbs()) + add_devices(WinkLight(light, hass) for light in pywink.get_bulbs()) class WinkLight(WinkDevice, Light): """Representation of a Wink light.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the Wink device.""" - WinkDevice.__init__(self, wink) + WinkDevice.__init__(self, wink, hass) @property def is_on(self): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 2e44c277b02..4536387e4ac 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -15,15 +15,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink platform.""" import pywink - add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) + add_devices(WinkLockDevice(lock, hass) for lock in pywink.get_locks()) class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the lock.""" - WinkDevice.__init__(self, wink) + WinkDevice.__init__(self, wink, hass) @property def is_locked(self): diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 455b3b03290..379d9ac43e5 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -22,24 +22,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for sensor in pywink.get_sensors(): if sensor.capability() in SENSOR_TYPES: - add_devices([WinkSensorDevice(sensor)]) + add_devices([WinkSensorDevice(sensor, hass)]) - add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays()) + for eggtray in pywink.get_eggtrays(): + add_devices([WinkEggMinder(eggtray, hass)]) for piggy_bank in pywink.get_piggy_banks(): try: if piggy_bank.capability() in SENSOR_TYPES: - add_devices([WinkSensorDevice(piggy_bank)]) + add_devices([WinkSensorDevice(piggy_bank, hass)]) except AttributeError: - logging.getLogger(__name__).error("Device is not a sensor") + logging.getLogger(__name__).info("Device is not a sensor") class WinkSensorDevice(WinkDevice, Entity): """Representation of a Wink sensor.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the Wink device.""" - super().__init__(wink) + super().__init__(wink, hass) wink = get_component('wink') self.capability = self.wink.capability() if self.wink.UNIT == '°': @@ -84,9 +85,9 @@ class WinkSensorDevice(WinkDevice, Entity): class WinkEggMinder(WinkDevice, Entity): """Representation of a Wink Egg Minder.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the sensor.""" - WinkDevice.__init__(self, wink) + WinkDevice.__init__(self, wink, hass) @property def state(self): diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 8bae64bbf99..22793d81f3f 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -15,18 +15,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink platform.""" import pywink - add_devices(WinkToggleDevice(switch) for switch in pywink.get_switches()) - add_devices(WinkToggleDevice(switch) for switch in - pywink.get_powerstrip_outlets()) - add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens()) + for switch in pywink.get_switches(): + add_devices([WinkToggleDevice(switch, hass)]) + for switch in pywink.get_powerstrip_outlets(): + add_devices([WinkToggleDevice(switch, hass)]) + for switch in pywink.get_sirens(): + add_devices([WinkToggleDevice(switch, hass)]) class WinkToggleDevice(WinkDevice, ToggleEntity): """Representation of a Wink toggle device.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the Wink device.""" - WinkDevice.__init__(self, wink) + WinkDevice.__init__(self, wink, hass) @property def is_on(self): diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index c678024f6a3..dbd7d8760ce 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -5,17 +5,18 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/wink/ """ import logging -import json import voluptuous as vol from homeassistant.helpers import discovery from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL, \ - CONF_EMAIL, CONF_PASSWORD + CONF_EMAIL, CONF_PASSWORD, \ + EVENT_HOMEASSISTANT_START, \ + EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-wink==0.10.0', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.10.1', 'pubnubsub-handler==0.0.5'] _LOGGER = logging.getLogger(__name__) @@ -57,13 +58,13 @@ WINK_COMPONENTS = [ def setup(hass, config): """Setup the Wink component.""" import pywink + from pubnubsubhandler import PubNubSubscriptionHandler - user_agent = config[DOMAIN][CONF_USER_AGENT] + user_agent = config[DOMAIN].get(CONF_USER_AGENT) if user_agent: pywink.set_user_agent(user_agent) - from pubnub import Pubnub access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) if access_token: @@ -76,49 +77,62 @@ def setup(hass, config): pywink.set_wink_credentials(email, password, client_id, client_secret) - global SUBSCRIPTION_HANDLER - SUBSCRIPTION_HANDLER = Pubnub( - 'N/A', pywink.get_subscription_key(), ssl_on=True) - SUBSCRIPTION_HANDLER.set_heartbeat(120) + hass.data[DOMAIN] = {} + hass.data[DOMAIN]['entities'] = [] + hass.data[DOMAIN]['pubnub'] = PubNubSubscriptionHandler( + pywink.get_subscription_key(), + pywink.wink_api_fetch) + + def start_subscription(event): + """Start the pubnub subscription.""" + hass.data[DOMAIN]['pubnub'].subscribe() + hass.bus.listen(EVENT_HOMEASSISTANT_START, start_subscription) + + def stop_subscription(event): + """Stop the pubnub subscription.""" + hass.data[DOMAIN]['pubnub'].unsubscribe() + hass.bus.listen(EVENT_HOMEASSISTANT_STOP, stop_subscription) + + def force_update(call): + """Force all devices to poll the Wink API.""" + _LOGGER.info("Refreshing Wink states from API.") + for entity in hass.data[DOMAIN]['entities']: + entity.update_ha_state(True) + hass.services.register(DOMAIN, 'Refresh state from Wink', force_update) # Load components for the devices in Wink that we support for component in WINK_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) + return True class WinkDevice(Entity): """Representation a base Wink device.""" - def __init__(self, wink): + def __init__(self, wink, hass): """Initialize the Wink device.""" - from pubnub import Pubnub self.wink = wink self._battery = self.wink.battery_level - if self.wink.pubnub_channel in CHANNELS: - pubnub = Pubnub('N/A', self.wink.pubnub_key, ssl_on=True) - pubnub.set_heartbeat(120) - pubnub.subscribe(self.wink.pubnub_channel, - self._pubnub_update, - error=self._pubnub_error) - else: - CHANNELS.append(self.wink.pubnub_channel) - SUBSCRIPTION_HANDLER.subscribe(self.wink.pubnub_channel, - self._pubnub_update, - error=self._pubnub_error) + hass.data[DOMAIN]['pubnub'].add_subscription( + self.wink.pubnub_channel, + self._pubnub_update) + hass.data[DOMAIN]['entities'].append(self) - def _pubnub_update(self, message, channel): + def _pubnub_update(self, message): try: - self.wink.pubnub_update(json.loads(message)) - self.update_ha_state() - except (AttributeError, KeyError): - error = "Pubnub returned invalid json for " + self.name - logging.getLogger(__name__).error(error) + if message is None: + _LOGGER.error("Error on pubnub update for " + self.name + + " pollin API for current state") + self.update_ha_state(True) + else: + self.wink.pubnub_update(message) + self.update_ha_state() + except (ValueError, KeyError, AttributeError): + _LOGGER.error("Error in pubnub JSON for " + self.name + + " pollin API for current state") self.update_ha_state(True) - def _pubnub_error(self, message): - _LOGGER.error("Error on pubnub update for " + self.wink.name()) - @property def unique_id(self): """Return the ID of this Wink device.""" diff --git a/requirements_all.txt b/requirements_all.txt index ad1a41b457b..1cbcf58c654 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -340,7 +340,7 @@ proliphix==0.4.1 psutil==5.0.0 # homeassistant.components.wink -pubnub==3.8.2 +pubnubsub-handler==0.0.5 # homeassistant.components.notify.pushbullet pushbullet.py==0.10.0 @@ -465,7 +465,7 @@ python-telegram-bot==5.2.0 python-twitch==1.3.0 # homeassistant.components.wink -python-wink==0.10.0 +python-wink==0.10.1 # homeassistant.components.keyboard # pyuserinput==0.1.11 From bde7176b3c9e4059be3cbe8be7a91377b902eb82 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 30 Nov 2016 22:33:38 +0100 Subject: [PATCH 104/137] Migrate light component to async (#4635) --- homeassistant/components/light/__init__.py | 146 ++++++++++-------- homeassistant/components/light/demo.py | 4 +- .../components/light/limitlessled.py | 2 +- homeassistant/components/light/mqtt.py | 4 +- homeassistant/components/light/mqtt_json.py | 4 +- .../components/light/mqtt_template.py | 4 +- homeassistant/components/light/mysensors.py | 8 +- .../components/light/osramlightify.py | 4 +- homeassistant/components/light/scsgate.py | 4 +- homeassistant/components/light/vera.py | 4 +- tests/components/light/test_init.py | 3 +- 11 files changed, 99 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c4ed91af0af..ff7121feaaa 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -4,6 +4,7 @@ Provides functionality to interact with lights. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light/ """ +import asyncio import logging import os import csv @@ -163,7 +164,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, ] if value is not None } - hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) def turn_off(hass, entity_id=None, transition=None): @@ -182,8 +183,8 @@ def async_turn_off(hass, entity_id=None, transition=None): ] if value is not None } - hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF, - data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data)) def toggle(hass, entity_id=None, transition=None): @@ -198,13 +199,83 @@ def toggle(hass, entity_id=None, transition=None): hass.services.call(DOMAIN, SERVICE_TOGGLE, data) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Expose light control via statemachine and services.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) - component.setup(config) + yield from component.async_setup(config) - # Load built-in profiles and custom profiles + # load profiles from files + profiles = yield from hass.loop.run_in_executor( + None, _load_profile_data, hass) + + if profiles is None: + return False + + @asyncio.coroutine + def async_handle_light_service(service): + """Hande a turn light on or off service call.""" + # Get the validated data + params = service.data.copy() + + # Convert the entity ids to valid light ids + target_lights = component.async_extract_from_service(service) + params.pop(ATTR_ENTITY_ID, None) + + # Processing extra data for turn light on request. + profile = profiles.get(params.pop(ATTR_PROFILE, None)) + + if profile: + params.setdefault(ATTR_XY_COLOR, profile[:2]) + params.setdefault(ATTR_BRIGHTNESS, profile[2]) + + color_name = params.pop(ATTR_COLOR_NAME, None) + + if color_name is not None: + params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) + + update_tasks = [] + for light in target_lights: + if service.service == SERVICE_TURN_ON: + yield from light.async_turn_on(**params) + elif service.service == SERVICE_TURN_OFF: + yield from light.async_turn_off(**params) + else: + yield from light.async_toggle(**params) + + if light.should_poll: + update_coro = light.async_update_ha_state(True) + if hasattr(light, 'async_update'): + update_tasks.append(hass.loop.create_task(update_coro)) + else: + yield from update_coro + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + # Listen for light on and light off service calls. + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_light_service, + descriptions.get(SERVICE_TURN_ON), schema=LIGHT_TURN_ON_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_handle_light_service, + descriptions.get(SERVICE_TURN_OFF), schema=LIGHT_TURN_OFF_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, async_handle_light_service, + descriptions.get(SERVICE_TOGGLE), schema=LIGHT_TOGGLE_SCHEMA) + + return True + + +def _load_profile_data(hass): + """Load built-in profiles and custom profiles.""" profile_paths = [os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), hass.config.path(LIGHT_PROFILES_FILE)] @@ -226,67 +297,8 @@ def setup(hass, config): except vol.MultipleInvalid as ex: _LOGGER.error("Error parsing light profile from %s: %s", profile_path, ex) - return False - - def handle_light_service(service): - """Hande a turn light on or off service call.""" - # Get the validated data - params = service.data.copy() - - # Convert the entity ids to valid light ids - target_lights = component.extract_from_service(service) - params.pop(ATTR_ENTITY_ID, None) - - service_fun = None - if service.service == SERVICE_TURN_OFF: - service_fun = 'turn_off' - elif service.service == SERVICE_TOGGLE: - service_fun = 'toggle' - - if service_fun: - for light in target_lights: - getattr(light, service_fun)(**params) - - for light in target_lights: - if light.should_poll: - light.update_ha_state(True) - return - - # Processing extra data for turn light on request. - profile = profiles.get(params.pop(ATTR_PROFILE, None)) - - if profile: - params.setdefault(ATTR_XY_COLOR, profile[:2]) - params.setdefault(ATTR_BRIGHTNESS, profile[2]) - - color_name = params.pop(ATTR_COLOR_NAME, None) - - if color_name is not None: - params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) - - for light in target_lights: - light.turn_on(**params) - - for light in target_lights: - if light.should_poll: - light.update_ha_state(True) - - # Listen for light on and light off service calls. - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service, - descriptions.get(SERVICE_TURN_ON), - schema=LIGHT_TURN_ON_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service, - descriptions.get(SERVICE_TURN_OFF), - schema=LIGHT_TURN_OFF_SCHEMA) - - hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service, - descriptions.get(SERVICE_TOGGLE), - schema=LIGHT_TOGGLE_SCHEMA) - - return True + return None + return profiles class Light(ToggleEntity): diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index b6048da243d..614374ce65f 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -129,9 +129,9 @@ class DemoLight(Light): if ATTR_EFFECT in kwargs: self._effect = kwargs[ATTR_EFFECT] - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the light off.""" self._state = False - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 8e0ea5cee83..d9d2407c98c 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -140,7 +140,7 @@ def state(new_state): # Update state. self._is_on = new_state self.group.enqueue(pipeline) - self.update_ha_state() + self.schedule_update_ha_state() return wrapper return decorator diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 424d0a5451c..54fa6b30598 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -283,7 +283,7 @@ class MqttLight(Light): should_update = True if should_update: - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -293,4 +293,4 @@ class MqttLight(Light): if self._optimistic: # Optimistically assume that switch has changed state. self._state = False - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 6f1d4e13e7b..d26f5490049 100755 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -216,7 +216,7 @@ class MqttJson(Light): should_update = True if should_update: - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -231,4 +231,4 @@ class MqttJson(Light): if self._optimistic: # Optimistically assume that the light has changed state. self._state = False - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index f632ba37236..55d4afac231 100755 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -263,7 +263,7 @@ class MqttTemplate(Light): ) if self._optimistic: - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the entity off.""" @@ -283,4 +283,4 @@ class MqttTemplate(Light): ) if self._optimistic: - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 20da91682ad..86d033cf4ce 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -115,7 +115,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._state = True self._values[set_req.V_LIGHT] = STATE_ON - self.update_ha_state() + self.schedule_update_ha_state() def _turn_on_dimmer(self, **kwargs): """Turn on dimmer child device.""" @@ -135,7 +135,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): # optimistically assume that light has changed state self._brightness = brightness self._values[set_req.V_DIMMER] = percent - self.update_ha_state() + self.schedule_update_ha_state() def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" @@ -165,7 +165,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): self._white = white if hex_color: self._values[self.value_type] = hex_color - self.update_ha_state() + self.schedule_update_ha_state() def _turn_off_light(self, value_type=None, value=None): """Turn off light child device.""" @@ -211,7 +211,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): self._state = False self._values[value_type] = ( STATE_OFF if set_req.V_LIGHT in self._values else value) - self.update_ha_state() + self.schedule_update_ha_state() def _update_light(self): """Update the controller with values from light child.""" diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 55d96236f2d..b4c593d8395 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -189,7 +189,7 @@ class OsramLightifyLight(Light): " %s with transition %s ", self._name, transition) - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -209,7 +209,7 @@ class OsramLightifyLight(Light): self._light.set_onoff(0) self._state = self._light.on() - self.update_ha_state() + self.schedule_update_ha_state() def update(self): """Synchronize state with bridge.""" diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py index a33b30736fe..7445977c4f3 100644 --- a/homeassistant/components/light/scsgate.py +++ b/homeassistant/components/light/scsgate.py @@ -84,7 +84,7 @@ class SCSGateLight(Light): ToggleStatusTask(target=self._scs_id, toggled=True)) self._toggled = True - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" @@ -94,7 +94,7 @@ class SCSGateLight(Light): ToggleStatusTask(target=self._scs_id, toggled=False)) self._toggled = False - self.update_ha_state() + self.schedule_update_ha_state() def process_event(self, message): """Handle a SCSGate message related with this light.""" diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 59b309e42aa..0508e654f43 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -53,13 +53,13 @@ class VeraLight(VeraDevice, Light): self.vera_device.switch_on() self._state = STATE_ON - self.update_ha_state(True) + self.schedule_update_ha_state(True) def turn_off(self, **kwargs): """Turn the light off.""" self.vera_device.switch_off() self._state = STATE_OFF - self.update_ha_state() + self.schedule_update_ha_state() @property def is_on(self): diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 60e5b4d9ec2..757f144ca57 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -271,8 +271,7 @@ class TestLight(unittest.TestCase): user_file.write('I,WILL,NOT,WORK\n') self.assertFalse(setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: 'test'}} - )) + self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: 'test'}})) def test_light_profiles(self): """Test light profiles.""" From c6c8cd4f51f7d7d0214d7d2844bb10e8a59b0b4f Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 1 Dec 2016 08:20:21 +0200 Subject: [PATCH 105/137] Yr.no: New aiohttp client needs params to form websession URL (#4634) * Yr.no: New aiohttp client needs params to form websession URL * Support params in aiohttp mocking --- homeassistant/components/sensor/yr.py | 13 ++++++++----- tests/test_util/aiohttp.py | 15 +++++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 4eeb809b7cf..d2b3a35816d 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -70,7 +70,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - coordinates = dict(lat=latitude, lon=longitude, msl=elevation) + coordinates = {'lat': str(latitude), + 'lon': str(longitude), + 'msl': str(elevation)} dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: @@ -135,8 +137,8 @@ class YrData(object): def __init__(self, hass, coordinates, devices): """Initialize the data object.""" - self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ - 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) + self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/' + self._urlparams = coordinates self._nextrun = None self.devices = devices self.data = {} @@ -159,9 +161,10 @@ class YrData(object): try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): - resp = yield from websession.get(self._url) + resp = yield from websession.get(self._url, + params=self._urlparams) if resp.status != 200: - try_again('{} returned {}'.format(self._url, resp.status)) + try_again('{} returned {}'.format(resp.url, resp.status)) return text = yield from resp.text() diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 7cf0fe9378d..bbaf62a8680 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -5,6 +5,7 @@ import functools import json as _json from unittest import mock from urllib.parse import urlparse, parse_qs +import yarl class AiohttpClientMocker: @@ -20,7 +21,8 @@ class AiohttpClientMocker: status=200, text=None, content=None, - json=None): + json=None, + params=None): """Mock a request.""" if json: text = _json.dumps(json) @@ -28,6 +30,8 @@ class AiohttpClientMocker: content = text.encode('utf-8') if content is None: content = b'' + if params: + url = str(yarl.URL(url).with_query(params)) self._mocks.append(AiohttpClientMockResponse( method, url, status, content)) @@ -58,11 +62,11 @@ class AiohttpClientMocker: return len(self.mock_calls) @asyncio.coroutine - def match_request(self, method, url, *, auth=None): \ + def match_request(self, method, url, *, auth=None, params=None): \ # pylint: disable=unused-variable """Match a request against pre-registered requests.""" for response in self._mocks: - if response.match_request(method, url): + if response.match_request(method, url, params): self.mock_calls.append((method, url)) return response @@ -82,11 +86,14 @@ class AiohttpClientMockResponse: self.status = status self.response = response - def match_request(self, method, url): + def match_request(self, method, url, params=None): """Test if response answers request.""" if method.lower() != self.method.lower(): return False + if params: + url = str(yarl.URL(url).with_query(params)) + # regular expression matching if self._url_parts is None: return self._url.search(url) is not None From 6dfae7a259212222c3bd1703c95df2d77ac08664 Mon Sep 17 00:00:00 2001 From: John Mihalic Date: Thu, 1 Dec 2016 02:58:16 -0500 Subject: [PATCH 106/137] Add support for NUT (Network UPS Tools) sensor. (#4551) * Add support for NUT (Network UPS Tools) sensor. * Address comments * Fix issues * Fix issues 2 * Fix unhandled exception --- .coveragerc | 1 + homeassistant/components/sensor/nut.py | 289 +++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 293 insertions(+) create mode 100644 homeassistant/components/sensor/nut.py diff --git a/.coveragerc b/.coveragerc index 8e01d88154f..37d412b63d4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -280,6 +280,7 @@ omit = homeassistant/components/sensor/miflora.py homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/neurio_energy.py + homeassistant/components/sensor/nut.py homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/ohmconnect.py homeassistant/components/sensor/onewire.py diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py new file mode 100644 index 00000000000..28a23152dc1 --- /dev/null +++ b/homeassistant/components/sensor/nut.py @@ -0,0 +1,289 @@ +""" +Provides a sensor to track various status aspects of a UPS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.nut/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, + TEMP_CELSIUS, CONF_RESOURCES, CONF_ALIAS, ATTR_STATE, STATE_UNKNOWN) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pynut2==2.1.2'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'NUT UPS' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 3493 + +KEY_STATUS = 'ups.status' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +SENSOR_TYPES = { + 'ups.status': ['Status Data', '', 'mdi:information-outline'], + 'ups.alarm': ['Alarms', '', 'mdi:alarm'], + 'ups.time': ['Internal Time', '', 'mdi:calendar-clock'], + 'ups.date': ['Internal Date', '', 'mdi:calendar'], + 'ups.model': ['Model', '', 'mdi:information-outline'], + 'ups.mfr': ['Manufacturer', '', 'mdi:information-outline'], + 'ups.mfr.date': ['Manufacture Date', '', 'mdi:calendar'], + 'ups.serial': ['Serial Number', '', 'mdi:information-outline'], + 'ups.vendorid': ['Vendor ID', '', 'mdi:information-outline'], + 'ups.productid': ['Product ID', '', 'mdi:information-outline'], + 'ups.firmware': ['Firmware Version', '', 'mdi:information-outline'], + 'ups.firmware.aux': ['Firmware Version 2', '', 'mdi:information-outline'], + 'ups.temperature': ['UPS Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + 'ups.load': ['Load', '%', 'mdi:gauge'], + 'ups.load.high': ['Overload Setting', '%', 'mdi:gauge'], + 'ups.id': ['System identifier', '', 'mdi:information-outline'], + 'ups.delay.start': ['Load Restart Delay', 'sec', 'mdi:timer'], + 'ups.delay.reboot': ['UPS Reboot Delay', 'sec', 'mdi:timer'], + 'ups.delay.shutdown': ['UPS Shutdown Delay', 'sec', 'mdi:timer'], + 'ups.timer.start': ['Load Start Timer', 'sec', 'mdi:timer'], + 'ups.timer.reboot': ['Load Reboot Timer', 'sec', 'mdi:timer'], + 'ups.timer.shutdown': ['Load Shutdown Timer', 'sec', 'mdi:timer'], + 'ups.test.interval': ['Self-Test Interval', 'sec', 'mdi:timer'], + 'ups.test.result': ['Self-Test Result', '', 'mdi:information-outline'], + 'ups.test.date': ['Self-Test Date', '', 'mdi:calendar'], + 'ups.display.language': ['Language', '', 'mdi:information-outline'], + 'ups.contacts': ['External Contacts', '', 'mdi:information-outline'], + 'ups.efficiency': ['Efficiency', '%', 'mdi:gauge'], + 'ups.power': ['Current Apparent Power', 'VA', 'mdi:flash'], + 'ups.power.nominal': ['Nominal Power', 'VA', 'mdi:flash'], + 'ups.realpower': ['Current Real Power', 'W', 'mdi:flash'], + 'ups.realpower.nominal': ['Nominal Real Power', 'W', 'mdi:flash'], + 'ups.beeper.status': ['Beeper Status', '', 'mdi:information-outline'], + 'ups.type': ['UPS Type', '', 'mdi:information-outline'], + 'ups.watchdog.status': ['Watchdog Status', '', 'mdi:information-outline'], + 'ups.start.auto': ['Start on AC', '', 'mdi:information-outline'], + 'ups.start.battery': ['Start on Battery', '', 'mdi:information-outline'], + 'ups.start.reboot': ['Reboot on Battery', '', 'mdi:information-outline'], + 'ups.shutdown': ['Shutdown Ability', '', 'mdi:information-outline'], + 'battery.charge': ['Battery Charge', '%', 'mdi:gauge'], + 'battery.charge.low': ['Low Battery Setpoint', '%', 'mdi:gauge'], + 'battery.charge.restart': ['Minimum Battery to Start', '%', 'mdi:gauge'], + 'battery.charge.warning': ['Warning Battery Setpoint', '%', 'mdi:gauge'], + 'battery.charger.status': + ['Charging Status', '', 'mdi:information-outline'], + 'battery.voltage': ['Battery Voltage', 'V', 'mdi:flash'], + 'battery.voltage.nominal': ['Nominal Battery Voltage', 'V', 'mdi:flash'], + 'battery.voltage.low': ['Low Battery Voltage', 'V', 'mdi:flash'], + 'battery.voltage.high': ['High Battery Voltage', 'V', 'mdi:flash'], + 'battery.capacity': ['Battery Capacity', 'Ah', 'mdi:flash'], + 'battery.current': ['Battery Current', 'A', 'mdi:flash'], + 'battery.current.total': ['Total Battery Current', 'A', 'mdi:flash'], + 'battery.temperature': + ['Battery Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + 'battery.runtime': ['Battery Runtime', 'sec', 'mdi:timer'], + 'battery.runtime.low': ['Low Battery Runtime', 'sec', 'mdi:timer'], + 'battery.runtime.restart': + ['Minimum Battery Runtime to Start', 'sec', 'mdi:timer'], + 'battery.alarm.threshold': + ['Battery Alarm Threshold', '', 'mdi:information-outline'], + 'battery.date': ['Battery Date', '', 'mdi:calendar'], + 'battery.mfr.date': ['Battery Manuf. Date', '', 'mdi:calendar'], + 'battery.packs': ['Number of Batteries', '', 'mdi:information-outline'], + 'battery.packs.bad': + ['Number of Bad Batteries', '', 'mdi:information-outline'], + 'battery.type': ['Battery Chemistry', '', 'mdi:information-outline'], + 'input.sensitivity': + ['Input Power Sensitivity', '', 'mdi:information-outline'], + 'input.transfer.low': ['Low Voltage Transfer', 'V', 'mdi:flash'], + 'input.transfer.high': ['High Voltage Transfer', 'V', 'mdi:flash'], + 'input.transfer.reason': + ['Voltage Transfer Reason', '', 'mdi:information-outline'], + 'input.voltage': ['Input Voltage', 'V', 'mdi:flash'], + 'input.voltage.nominal': ['Nominal Input Voltage', 'V', 'mdi:flash'], +} + +STATE_TYPES = { + 'OL': 'Online', + 'OB': 'On Battery', + 'LB': 'Low Battery', + 'HB': 'High Battery', + 'RB': 'Battery Needs Replaced', + 'CHRG': 'Battery Charging', + 'BYPASS': 'Bypass Active', + 'CAL': 'Runtime Calibration', + 'OFF': 'Offline', + 'OVER': 'Overloaded', + 'TRIM': 'Trimming Voltage', + 'BOOST': 'Boosting Voltage', + 'FSD': 'Forced Shutdown', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_ALIAS, default=None): cv.string, + vol.Optional(CONF_USERNAME, default=None): cv.string, + vol.Optional(CONF_PASSWORD, default=None): cv.string, + vol.Required(CONF_RESOURCES, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Setup the NUT sensors.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + alias = config.get(CONF_ALIAS) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + data = PyNUTData(host, port, alias, username, password) + + if data.status is None: + _LOGGER.error("NUT Sensor has no data, unable to setup.") + return False + + _LOGGER.debug('NUT Sensors Available: %s', data.status) + + entities = [] + + for resource in config[CONF_RESOURCES]: + sensor_type = resource.lower() + + if sensor_type in data.status: + entities.append(NUTSensor(name, data, sensor_type)) + else: + _LOGGER.warning( + 'Sensor type: "%s" does not appear in the NUT status ' + 'output, cannot add.', sensor_type) + + try: + data.update(no_throttle=True) + except data.pynuterror as err: + _LOGGER.error("Failure while testing NUT status retrieval. " + "Cannot continue setup., %s", err) + return False + + add_entities(entities) + + +class NUTSensor(Entity): + """Representation of a sensor entity for NUT status values.""" + + def __init__(self, name, data, sensor_type): + """Initialize the sensor.""" + self._data = data + self.type = sensor_type + self._name = name + ' ' + SENSOR_TYPES[sensor_type][0] + self._unit = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + """Return the name of the UPS sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return entity state from ups.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the sensor attributes.""" + attr = {} + attr[ATTR_STATE] = self.opp_state() + return attr + + def opp_state(self): + """Return UPS operating state.""" + if self._data.status is None: + return STATE_TYPES['OFF'] + else: + try: + return STATE_TYPES[self._data.status[KEY_STATUS]] + except KeyError: + return STATE_UNKNOWN + + def update(self): + """Get the latest status and use it to update our sensor state.""" + if self._data.status is None: + self._state = None + return + + if self.type not in self._data.status: + self._state = None + else: + self._state = self._data.status[self.type] + + +class PyNUTData(object): + """Stores the data retrieved from NUT. + + For each entity to use, acts as the single point responsible for fetching + updates from the server. + """ + + def __init__(self, host, port, alias, username, password): + """Initialize the data oject.""" + from pynut2.nut2 import PyNUTClient, PyNUTError + self._host = host + self._port = port + self._alias = alias + self._username = username + self._password = password + + self.pynuterror = PyNUTError + # Establish client with persistent=False to open/close connection on + # each update call. This is more reliable with async. + self._client = PyNUTClient(self._host, self._port, + self._username, self._password, 5, False) + + self._status = None + + @property + def status(self): + """Get latest update if throttle allows. Return status.""" + self.update() + return self._status + + def _get_alias(self): + """Get the ups alias from NUT.""" + try: + return next(iter(self._client.list_ups())) + except self.pynuterror as err: + _LOGGER.error("Failure getting NUT ups alias, %s", err) + return None + + def _get_status(self): + """Get the ups status from NUT.""" + if self._alias is None: + self._alias = self._get_alias() + + try: + return self._client.list_vars(self._alias) + except (self.pynuterror, ConnectionResetError) as err: + _LOGGER.debug("Error getting NUT vars for host %s: %s", + self._host, err) + return None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """Fetch the latest status from APCUPSd.""" + self._status = self._get_status() diff --git a/requirements_all.txt b/requirements_all.txt index 1cbcf58c654..8da898fb3d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -416,6 +416,9 @@ pynetgear==0.3.3 # homeassistant.components.switch.netio pynetio==0.1.6 +# homeassistant.components.sensor.nut +pynut2==2.1.2 + # homeassistant.components.alarm_control_panel.nx584 # homeassistant.components.binary_sensor.nx584 pynx584==0.2 From dd84b4e237f00129e04f1f78aa6ebb1b556ab943 Mon Sep 17 00:00:00 2001 From: Jan Losinski Date: Thu, 1 Dec 2016 21:20:42 +0100 Subject: [PATCH 107/137] Mpd: Use "file" instead "id" for media_content_id (#4653) In media_content_id() the "id" of the current song was returned. as stated in bug #4652 the id is only the Tracklist-Id in the current tracklist and is omitted if the track is not part of a tracklist (what caused the bug in the first place). To match the semantics described in the dockstring, to return a "Content ID", this chooses the filename of the current song as id and returns it. It also uses get() instead of [] to prevent KeyError. This fixes bug #4652 Signed-off-by: Jan Losinski --- homeassistant/components/media_player/mpd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 9967a26a3a0..083b1108f92 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -133,7 +133,7 @@ class MpdDevice(MediaPlayerDevice): @property def media_content_id(self): """Content ID of current playing media.""" - return self.currentsong['id'] + return self.currentsong.get('file') @property def media_content_type(self): From 5c807c6bd9e6c2a0bce2bd50fd3d0f573d85b8b2 Mon Sep 17 00:00:00 2001 From: Jan Losinski Date: Thu, 1 Dec 2016 21:28:31 +0100 Subject: [PATCH 108/137] MPD: Reconnect mpd client afetr OSError (#4651) If the mpd client ran into an socket timeout, the socket will raise an OSError on every further request. This adds OSError to the list of excptions, that causes a client reconnect. This fixes #4650 Signed-off-by: Jan Losinski --- homeassistant/components/media_player/mpd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 083b1108f92..acfd0e9307c 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -100,7 +100,7 @@ class MpdDevice(MediaPlayerDevice): try: self.status = self.client.status() self.currentsong = self.client.currentsong() - except (mpd.ConnectionError, BrokenPipeError, ValueError): + except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError): # Cleanly disconnect in case connection is not in valid state try: self.client.disconnect() From 898f89ffc7444677dc52afec98d3087094c5a0b8 Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Thu, 1 Dec 2016 14:28:59 -0600 Subject: [PATCH 109/137] Make trusted_networks iterable (#4649) --- homeassistant/components/emulated_hue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index dcb6bcb64b2..7fd187daa4d 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -77,7 +77,7 @@ def setup(hass, yaml_config): ssl_key=None, cors_origins=None, use_x_forwarded_for=False, - trusted_networks=None, + trusted_networks=[], login_threshold=0, is_ban_enabled=False ) From de6c5a503be125606170f74156acad1443c4e7bf Mon Sep 17 00:00:00 2001 From: iandday Date: Thu, 1 Dec 2016 15:48:08 -0500 Subject: [PATCH 110/137] Remote Component and Harmony Platform (#4254) * Initial Harmony device support, working current activity sensor and switch for each activity TODO: add new device per hub to send device specific activity Changes to be committed: new file: homeassistant/components/harmony.py new file: homeassistant/components/sensor/harmony.py new file: homeassistant/components/switch/harmony.py * ready for beta, I think Changes to be committed: modified: homeassistant/components/harmony.py modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py * Changes to be committed: modified: homeassistant/components/harmony.py new file: homeassistant/components/remote/__init__.py new file: homeassistant/components/remote/harmony.py new file: homeassistant/components/remote/services.yaml modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py Implemented remote component and harmony platform * streamlined harmony support * typo * Initial Harmony device support, working current activity sensor and switch for each activity TODO: add new device per hub to send device specific activity Changes to be committed: new file: homeassistant/components/harmony.py new file: homeassistant/components/sensor/harmony.py new file: homeassistant/components/switch/harmony.py * ready for beta, I think Changes to be committed: modified: homeassistant/components/harmony.py modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py * Changes to be committed: modified: homeassistant/components/harmony.py new file: homeassistant/components/remote/__init__.py new file: homeassistant/components/remote/harmony.py new file: homeassistant/components/remote/services.yaml modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py Implemented remote component and harmony platform * streamlined harmony support * typo * reworked token generation * delete * Initial Harmony device support, working current activity sensor and switch for each activity TODO: add new device per hub to send device specific activity Changes to be committed: new file: homeassistant/components/harmony.py new file: homeassistant/components/sensor/harmony.py new file: homeassistant/components/switch/harmony.py * Initial Harmony device support, working current activity sensor and switch for each activity TODO: add new device per hub to send device specific activity Changes to be committed: new file: homeassistant/components/harmony.py new file: homeassistant/components/sensor/harmony.py new file: homeassistant/components/switch/harmony.py * ready for beta, I think Changes to be committed: modified: homeassistant/components/harmony.py modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py * ready for beta, I think Changes to be committed: modified: homeassistant/components/harmony.py modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py * Changes to be committed: modified: homeassistant/components/harmony.py new file: homeassistant/components/remote/__init__.py new file: homeassistant/components/remote/harmony.py new file: homeassistant/components/remote/services.yaml modified: homeassistant/components/sensor/harmony.py modified: homeassistant/components/switch/harmony.py Implemented remote component and harmony platform * streamlined harmony support * typo * reworked token generation * delete * readded after rebase * cleaning up style errors * modified .coveragerc * moved import statements * added more debug logging * Added URL encoding of token received from Logitech * Corrected import for python 3 * new pyharmony version * new pyharmony version * remote tests * only write config file if not present or sync service is called * more tests * more tests * bumped pyharmony version to work with new auth * bumped pyharmony version to work with new auth * style corrections * harmony local auth and remote demo platform * style fix * PR refinements and permission issues * forgot a blank line * removed sync test from test_init * removed sync test from test_init * visual indent * send_command test in demo platform --- .coveragerc | 1 + homeassistant/components/remote/__init__.py | 142 +++++++++++++ homeassistant/components/remote/demo.py | 57 +++++ homeassistant/components/remote/harmony.py | 198 ++++++++++++++++++ homeassistant/components/remote/services.yaml | 42 ++++ requirements_all.txt | 3 + tests/components/remote/__init__.py | 1 + tests/components/remote/test_demo.py | 117 +++++++++++ tests/components/remote/test_init.py | 99 +++++++++ 9 files changed, 660 insertions(+) create mode 100755 homeassistant/components/remote/__init__.py create mode 100644 homeassistant/components/remote/demo.py create mode 100755 homeassistant/components/remote/harmony.py create mode 100644 homeassistant/components/remote/services.yaml create mode 100755 tests/components/remote/__init__.py create mode 100755 tests/components/remote/test_demo.py create mode 100755 tests/components/remote/test_init.py diff --git a/.coveragerc b/.coveragerc index 37d412b63d4..d0967918a60 100644 --- a/.coveragerc +++ b/.coveragerc @@ -241,6 +241,7 @@ omit = homeassistant/components/notify/xmpp.py homeassistant/components/nuimo_controller.py homeassistant/components/openalpr.py + homeassistant/components/remote/harmony.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py new file mode 100755 index 00000000000..57d816fd0c9 --- /dev/null +++ b/homeassistant/components/remote/__init__.py @@ -0,0 +1,142 @@ +""" +Component to interface with universal remote control devices. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/remote/ +""" +from datetime import timedelta +import logging +import os + +import voluptuous as vol +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.components import group +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa + +ATTR_DEVICE = 'device' +ATTR_COMMAND = 'command' +ATTR_ACTIVITY = 'activity' +SERVICE_SEND_COMMAND = 'send_command' +SERVICE_SYNC = 'sync' + +DOMAIN = 'remote' +SCAN_INTERVAL = 30 + +GROUP_NAME_ALL_REMOTES = 'all remotes' +ENTITY_ID_ALL_REMOTES = group.ENTITY_ID_FORMAT.format('all_remotes') + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +REMOTE_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + +REMOTE_SERVICE_TURN_ON_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({ + vol.Optional(ATTR_ACTIVITY): cv.string +}) + +REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_DEVICE): cv.string, + vol.Required(ATTR_COMMAND): cv.string, +}) + +_LOGGER = logging.getLogger(__name__) + + +def is_on(hass, entity_id=None): + """Return if the remote is on based on the statemachine.""" + entity_id = entity_id or ENTITY_ID_ALL_REMOTES + return hass.states.is_state(entity_id, STATE_ON) + + +def turn_on(hass, activity=None, entity_id=None): + """Turn all or specified remote on.""" + data = {ATTR_ACTIVITY: activity} + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +def turn_off(hass, entity_id=None): + """Turn all or specified remote off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +def send_command(hass, device, command, entity_id=None): + """Send a command to a device.""" + data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command} + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) + + +def sync(hass, entity_id=None): + """Sync remote device.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_SYNC, data) + + +def setup(hass, config): + """Track states and offer events for remotes.""" + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES) + component.setup(config) + + def handle_remote_service(service): + """Handle calls to the remote services.""" + target_remotes = component.extract_from_service(service) + + activity_id = service.data.get(ATTR_ACTIVITY) + device = service.data.get(ATTR_DEVICE) + command = service.data.get(ATTR_COMMAND) + + for remote in target_remotes: + if service.service == SERVICE_TURN_ON: + remote.turn_on(activity=activity_id) + elif service.service == SERVICE_SEND_COMMAND: + remote.send_command(device=device, command=command) + elif service.service == SERVICE_SYNC: + remote.sync() + else: + remote.turn_off() + + if remote.should_poll: + remote.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_remote_service, + descriptions.get(SERVICE_TURN_OFF), + schema=REMOTE_SERVICE_SCHEMA) + hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_remote_service, + descriptions.get(SERVICE_TURN_ON), + schema=REMOTE_SERVICE_TURN_ON_SCHEMA) + hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, handle_remote_service, + descriptions.get(SERVICE_SEND_COMMAND), + schema=REMOTE_SERVICE_SEND_COMMAND_SCHEMA) + + return True + + +class RemoteDevice(ToggleEntity): + """Representation of a remote.""" + + def turn_on(self, **kwargs): + """Turn a device on with the remote.""" + raise NotImplementedError() + + def turn_off(self, **kwargs): + """Turn a device off with the remote.""" + raise NotImplementedError() + + def send_command(self, **kwargs): + """Send a command to a device.""" + raise NotImplementedError() diff --git a/homeassistant/components/remote/demo.py b/homeassistant/components/remote/demo.py new file mode 100644 index 00000000000..90c691a3d3c --- /dev/null +++ b/homeassistant/components/remote/demo.py @@ -0,0 +1,57 @@ +""" +Demo platform that has two fake remotes. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +from homeassistant.components.remote import RemoteDevice +from homeassistant.const import DEVICE_DEFAULT_NAME + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the demo remotes.""" + add_devices_callback([ + DemoRemote('Remote One', False, None), + DemoRemote('Remote Two', True, 'mdi:remote'), + ]) + + +class DemoRemote(RemoteDevice): + """Representation of a demo remote.""" + + def __init__(self, name, state, icon): + """Initialize the Demo Remote.""" + self._name = name or DEVICE_DEFAULT_NAME + self._state = state + self._icon = icon + + @property + def should_poll(self): + """No polling needed for a demo remote.""" + return False + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def is_on(self): + """Return true if remote is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the remote on.""" + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the remote off.""" + self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py new file mode 100755 index 00000000000..9e799fab066 --- /dev/null +++ b/homeassistant/components/remote/harmony.py @@ -0,0 +1,198 @@ +""" +Support for Harmony Hub devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/remote.harmony/ + +""" + +import logging +from os import path +import urllib.parse +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID) +from homeassistant.components.remote import PLATFORM_SCHEMA, DOMAIN +from homeassistant.util import slugify +from homeassistant.config import load_yaml_config_file +import homeassistant.components.remote as remote +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + + +REQUIREMENTS = ['pyharmony==1.0.12'] +_LOGGER = logging.getLogger(__name__) + +ATTR_DEVICE = 'device' +ATTR_COMMAND = 'command' +ATTR_ACTIVITY = 'activity' + +SERVICE_SYNC = 'harmony_sync' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.string, + vol.Required(ATTR_ACTIVITY, default=None): cv.string, +}) + +HARMONY_SYNC_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +# List of devices that have been registered +DEVICES = [] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Harmony platform.""" + import pyharmony + global DEVICES + + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + _LOGGER.info('Loading Harmony platform: ' + name) + + harmony_conf_file = hass.config.path('harmony_' + slugify(name) + '.conf') + + try: + _LOGGER.debug('calling pyharmony.ha_get_token for remote at: ' + + host + ':' + port) + token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port)) + except ValueError as err: + _LOGGER.critical(err.args[0] + ' for remote: ' + name) + return False + + _LOGGER.debug('received token: ' + token) + DEVICES = [HarmonyRemote(config.get(CONF_NAME), + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(ATTR_ACTIVITY), + harmony_conf_file, + token)] + add_devices(DEVICES, True) + register_services(hass) + return True + + +def register_services(hass): + """Register all services for harmony devices.""" + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_SYNC, + _sync_service, + descriptions.get(SERVICE_SYNC), + schema=HARMONY_SYNC_SCHEMA) + + +def _apply_service(service, service_func, *service_func_args): + """Internal func for applying a service.""" + entity_ids = service.data.get('entity_id') + + if entity_ids: + _devices = [device for device in DEVICES + if device.entity_id in entity_ids] + else: + _devices = DEVICES + + for device in _devices: + service_func(device, *service_func_args) + device.update_ha_state(True) + + +def _sync_service(service): + _apply_service(service, HarmonyRemote.sync) + + +class HarmonyRemote(remote.RemoteDevice): + """Remote representation used to control a Harmony device.""" + + def __init__(self, name, host, port, activity, out_path, token): + """Initialize HarmonyRemote class.""" + import pyharmony + from pathlib import Path + + _LOGGER.debug('HarmonyRemote device init started for: ' + name) + self._name = name + self._ip = host + self._port = port + self._state = None + self._current_activity = None + self._default_activity = activity + self._token = token + self._config_path = out_path + _LOGGER.debug('retrieving harmony config using token: ' + token) + self._config = pyharmony.ha_get_config(self._token, host, port) + if not Path(self._config_path).is_file(): + _LOGGER.debug('writing harmony configuration to file: ' + out_path) + pyharmony.ha_write_config_file(self._config, self._config_path) + + @property + def name(self): + """Return the Harmony device's name.""" + return self._name + + @property + def device_state_attributes(self): + """Add platform specific attributes.""" + return {'current_activity': self._current_activity} + + @property + def is_on(self): + """Return False if PowerOff is the current activity, otherwise True.""" + return self._current_activity != 'PowerOff' + + def update(self): + """Return current activity.""" + import pyharmony + name = self._name + _LOGGER.debug('polling ' + name + ' for current activity') + state = pyharmony.ha_get_current_activity(self._token, + self._config, + self._ip, + self._port) + _LOGGER.debug(name + '\'s current activity reported as: ' + state) + self._current_activity = state + self._state = bool(state != 'PowerOff') + + def turn_on(self, **kwargs): + """Start an activity from the Harmony device.""" + import pyharmony + if kwargs[ATTR_ACTIVITY]: + activity = kwargs[ATTR_ACTIVITY] + else: + activity = self._default_activity + + if activity: + pyharmony.ha_start_activity(self._token, + self._ip, + self._port, + self._config, + activity) + self._state = True + else: + _LOGGER.error('No activity specified with turn_on service') + + def turn_off(self): + """Start the PowerOff activity.""" + import pyharmony + pyharmony.ha_power_off(self._token, self._ip, self._port) + + def send_command(self, **kwargs): + """Send a command to one device.""" + import pyharmony + pyharmony.ha_send_command(self._token, self._ip, + self._port, kwargs[ATTR_DEVICE], + kwargs[ATTR_COMMAND]) + + def sync(self): + """Sync the Harmony device with the web service.""" + import pyharmony + _LOGGER.debug('syncing hub with Harmony servers') + pyharmony.ha_sync(self._token, self._ip, self._port) + self._config = pyharmony.ha_get_config(self._token, + self._ip, + self._port) + _LOGGER.debug('writing hub config to file: ' + self._config_path) + pyharmony.ha_write_config_file(self._config, self._config_path) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml new file mode 100644 index 00000000000..2023588fcc2 --- /dev/null +++ b/homeassistant/components/remote/services.yaml @@ -0,0 +1,42 @@ +# Describes the format for available remote services + +turn_on: + description: Semds the Power On Command + + fields: + entity_id: + description: Name(s) of entities to turn on + example: 'remote.family_room' + activity: + description: Activity ID or Activity Name to start + example: 'BedroomTV' + +turn_off: + description: Sends the Power Off Command + + fields: + entity_id: + description: Name(s) of entities to turn off + example: 'remote.family_room' + +send_command: + description: Semds a single command to a single device + + fields: + entity_id: + description: Name(s) of entities to send command from + example: 'remote.family_room' + device: + description: Device ID to send command to + example: '32756745' + command: + description: Command to send + example: 'Play' + +harmony_sync: + description: Syncs the remote's configuration + + fields: + entity_id: + description: Name(s) of entities to sync + example: 'remote.family_room' diff --git a/requirements_all.txt b/requirements_all.txt index 8da898fb3d0..0d3709509fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,6 +392,9 @@ pyenvisalink==1.9 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.remote.harmony +pyharmony==1.0.12 + # homeassistant.components.homematic pyhomematic==0.1.18 diff --git a/tests/components/remote/__init__.py b/tests/components/remote/__init__.py new file mode 100755 index 00000000000..77870a11f20 --- /dev/null +++ b/tests/components/remote/__init__.py @@ -0,0 +1 @@ +"""The tests for Remote platforms.""" diff --git a/tests/components/remote/test_demo.py b/tests/components/remote/test_demo.py new file mode 100755 index 00000000000..f43f9e8610c --- /dev/null +++ b/tests/components/remote/test_demo.py @@ -0,0 +1,117 @@ +"""The tests for the demo remote component.""" +# pylint: disable=protected-access +import unittest + +from homeassistant.bootstrap import setup_component +import homeassistant.components.remote as remote +from homeassistant.const import ( + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, + SERVICE_TURN_ON, SERVICE_TURN_OFF) +from tests.common import get_test_home_assistant, mock_service + +SERVICE_SYNC = 'sync' +SERVICE_SEND_COMMAND = 'send_command' + + +class TestDemoRemote(unittest.TestCase): + """Test the demo remote.""" + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.assertTrue(setup_component(self.hass, remote.DOMAIN, {'remote': { + 'platform': 'demo', + }})) + + # pylint: disable=invalid-name + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_methods(self): + """Test if methods call the services as expected.""" + + self.assertTrue( + setup_component(self.hass, remote.DOMAIN, + {remote.DOMAIN: {CONF_PLATFORM: 'demo'}})) + + # Test is_on + self.hass.states.set('remote.demo', STATE_ON) + self.assertTrue(remote.is_on(self.hass, 'remote.demo')) + + self.hass.states.set('remote.demo', STATE_OFF) + self.assertFalse(remote.is_on(self.hass, 'remote.demo')) + + self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_ON) + self.assertTrue(remote.is_on(self.hass)) + + self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_OFF) + self.assertFalse(remote.is_on(self.hass)) + + def test_services(self): + """Test the provided services.""" + + # Test turn_on + turn_on_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_TURN_ON) + + remote.turn_on( + self.hass, + entity_id='entity_id_val') + + self.hass.block_till_done() + + self.assertEqual(1, len(turn_on_calls)) + call = turn_on_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + + # Test turn_off + turn_off_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_TURN_OFF) + + remote.turn_off( + self.hass, entity_id='entity_id_val') + + self.hass.block_till_done() + + self.assertEqual(1, len(turn_off_calls)) + call = turn_off_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) + + # Test sync + sync_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_SYNC) + + remote.sync( + self.hass, entity_id='entity_id_val') + + self.hass.block_till_done() + + self.assertEqual(1, len(sync_calls)) + call = sync_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + self.assertEqual(SERVICE_SYNC, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) + + # Test send_command + send_command_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND) + + remote.send_command( + self.hass, entity_id='entity_id_val', + device='test_device', command='test_command') + + self.hass.block_till_done() + + self.assertEqual(1, len(send_command_calls)) + call = send_command_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + self.assertEqual(SERVICE_SEND_COMMAND, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py new file mode 100755 index 00000000000..799ed3b5ea7 --- /dev/null +++ b/tests/components/remote/test_init.py @@ -0,0 +1,99 @@ +"""The tests for the Remote component, adapted from Light Test.""" +# pylint: disable=protected-access + +import unittest + +from homeassistant.bootstrap import setup_component +from homeassistant.const import ( + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, + SERVICE_TURN_ON, SERVICE_TURN_OFF) +import homeassistant.components.remote as remote + +from tests.common import mock_service, get_test_home_assistant +TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}} +SERVICE_SYNC = 'sync' +SERVICE_SEND_COMMAND = 'send_command' + + +class TestRemote(unittest.TestCase): + """Test the remote module.""" + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + # pylint: disable=invalid-name + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_is_on(self): + """ Test is_on""" + self.hass.states.set('remote.test', STATE_ON) + self.assertTrue(remote.is_on(self.hass, 'remote.test')) + + self.hass.states.set('remote.test', STATE_OFF) + self.assertFalse(remote.is_on(self.hass, 'remote.test')) + + self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_ON) + self.assertTrue(remote.is_on(self.hass)) + + self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_OFF) + self.assertFalse(remote.is_on(self.hass)) + + def test_turn_on(self): + """ Test turn_on""" + turn_on_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_TURN_ON) + + remote.turn_on( + self.hass, + entity_id='entity_id_val') + + self.hass.block_till_done() + + self.assertEqual(1, len(turn_on_calls)) + call = turn_on_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + + def test_turn_off(self): + """ Test turn_off""" + turn_off_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_TURN_OFF) + + remote.turn_off( + self.hass, entity_id='entity_id_val') + + self.hass.block_till_done() + + self.assertEqual(1, len(turn_off_calls)) + call = turn_off_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) + + def test_send_command(self): + """ Test send_command""" + send_command_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND) + + remote.send_command( + self.hass, entity_id='entity_id_val', + device='test_device', command='test_command') + + self.hass.block_till_done() + + self.assertEqual(1, len(send_command_calls)) + call = send_command_calls[-1] + + self.assertEqual(remote.DOMAIN, call.domain) + self.assertEqual(SERVICE_SEND_COMMAND, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) + + def test_services(self): + """Test the provided services.""" + self.assertTrue(setup_component(self.hass, remote.DOMAIN, + TEST_PLATFORM)) From 279f82acc49ab3ccb65991559180b40c40e9a480 Mon Sep 17 00:00:00 2001 From: lichtteil Date: Fri, 2 Dec 2016 03:26:53 +0100 Subject: [PATCH 111/137] Mutate values for light color temperature and white value (#4660) * Mutate values for light color temperature and white value * Fix lenght of line * Fix under-indented line * Fix cgl --- homeassistant/components/light/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index ff7121feaaa..04eb8fabc68 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -101,9 +101,10 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ vol.Coerce(tuple)), ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)), - ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=color_util.HASS_COLOR_MIN, - max=color_util.HASS_COLOR_MAX)), - ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)), + ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), + vol.Range(min=color_util.HASS_COLOR_MIN, + max=color_util.HASS_COLOR_MAX)), + ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), ATTR_EFFECT: cv.string, }) From f09b888a8a18352dfee1373f108b0f6eec1b61ac Mon Sep 17 00:00:00 2001 From: Brandon Weeks Date: Thu, 1 Dec 2016 18:28:52 -0800 Subject: [PATCH 112/137] Fixes #3511 - handle multiple return values (#4659) --- homeassistant/components/media_player/denon.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 351d6c8c491..b167ee52808 100755 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -62,7 +62,14 @@ class DenonDevice(MediaPlayerDevice): def telnet_request(cls, telnet, command): """Execute `command` and return the response.""" telnet.write(command.encode('ASCII') + b'\r') - return telnet.read_until(b'\r', timeout=0.2).decode('ASCII').strip() + lines = [] + while True: + line = telnet.read_until(b'\r', timeout=0.2) + if not line: + break + lines.append(line.decode('ASCII').strip()) + + return lines[0] def telnet_command(self, command): """Establish a telnet connection and sends `command`.""" @@ -79,9 +86,6 @@ class DenonDevice(MediaPlayerDevice): return False self._pwstate = self.telnet_request(telnet, 'PW?') - # PW? sends also SISTATUS, which is not interesting - telnet.read_until(b"\r", timeout=0.2) - volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):] self._volume = int(volume_str) / 60 self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON') From 08f8e540e3b278504e89ca91bf2c2e7df9fbb040 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Fri, 2 Dec 2016 03:30:41 +0100 Subject: [PATCH 113/137] Macvendor (#4468) * Add MAC vendor lookup for device_tracker. * Test vendor mac lookup and fix device attribute. * Generate requirements. * Style. * Use hyphen instead of underscore to satisfy 'idna'. https://github.com/kjd/idna/issues/17 * Resort imports. * Refactor macvendor to use macvendors.com API instead of netaddr library. * Test vendor lookup using macvendors.com api. * Remove debugging. * Correct description. * No longer needed. * Device tracker is now an async component. Fix ddwrt tests. * Fix linting. * Add test case for error conditions. * There is no reason to retry failes vendor loopups as they won't be saved to the file anyways at that point. * Sorry, bad assumption, this only made things worse. * Wait for async parts during setup component to complete before asserting results. * Fix linting. * Is generated when running 'coverage html'. * Undo isort. * Make aioclient_mock exception more generic. * Only lookup mac vendor string with adding new device to known_devices.yaml. * Undo isort. * Revert unneeded change. * Adjust to use new websession pattern. * Always make sure to cleanup response. * Use correct function to release response. * Fix tests. --- .gitignore | 1 + .../components/device_tracker/__init__.py | 63 +++++++++++++++- tests/components/device_tracker/test_ddwrt.py | 13 ++++ tests/components/device_tracker/test_init.py | 75 +++++++++++++++++++ tests/test_util/aiohttp.py | 8 +- 5 files changed, 156 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 43eae33f554..aa27aa435bd 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ pip-log.txt .coverage .tox nosetests.xml +htmlcov/ # Translations *.mo diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index f985e21ec22..91f0720e927 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -10,6 +10,8 @@ import logging import os from typing import Any, Sequence, Callable +import aiohttp +import async_timeout import voluptuous as vol from homeassistant.bootstrap import ( @@ -19,6 +21,7 @@ from homeassistant.components import group, zone from homeassistant.components.discovery import SERVICE_NETGEAR from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType @@ -278,6 +281,9 @@ class DeviceTracker(object): yield from self.group.async_update_tracked_entity_ids( list(self.group.tracking) + [device.entity_id]) + # lookup mac vendor string to be stored in config + device.set_vendor_for_mac() + # update known_devices.yaml self.hass.async_add_job( self.async_update_config(self.hass.config.path(YAML_DEVICES), @@ -328,6 +334,7 @@ class Device(Entity): last_seen = None # type: dt_util.dt.datetime battery = None # type: str attributes = None # type: dict + vendor = None # type: str # Track if the last update of this device was HOME. last_update_home = False @@ -336,7 +343,7 @@ class Device(Entity): def __init__(self, hass: HomeAssistantType, consider_home: timedelta, track: bool, dev_id: str, mac: str, name: str=None, picture: str=None, gravatar: str=None, - hide_if_away: bool=False) -> None: + hide_if_away: bool=False, vendor: str=None) -> None: """Initialize a device.""" self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -362,6 +369,7 @@ class Device(Entity): self.config_picture = picture self.away_hide = hide_if_away + self.vendor = vendor @property def name(self): @@ -460,6 +468,53 @@ class Device(Entity): self._state = STATE_HOME self.last_update_home = True + @asyncio.coroutine + def set_vendor_for_mac(self): + """Set vendor string using api.macvendors.com.""" + self.vendor = yield from self.get_vendor_for_mac() + + @asyncio.coroutine + def get_vendor_for_mac(self): + """Try to find the vendor string for a given MAC address.""" + # can't continue without a mac + if not self.mac: + return None + + # prevent lookup of invalid macs + if not len(self.mac.split(':')) == 6: + return 'unknown' + + # we only need the first 3 bytes of the mac for a lookup + # this improves somewhat on privacy + oui_bytes = self.mac.split(':')[0:3] + # bytes like 00 get truncates to 0, API needs full bytes + oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) + url = 'http://api.macvendors.com/' + oui + resp = None + try: + websession = async_get_clientsession(self.hass) + + with async_timeout.timeout(5, loop=self.hass.loop): + resp = yield from websession.get(url) + # mac vendor found, response is the string + if resp.status == 200: + vendor_string = yield from resp.text() + return vendor_string + # if vendor is not known to the API (404) or there + # was a failure during the lookup (500); set vendor + # to something other then None to prevent retry + # as the value is only relevant when it is to be stored + # in the 'known_devices.yaml' file which only happens + # the first time the device is seen. + return 'unknown' + except (asyncio.TimeoutError, aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError): + # same as above + return 'unknown' + finally: + if resp is not None: + yield from resp.release() + def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): """Load devices from YAML configuration file.""" @@ -483,7 +538,8 @@ def async_load_config(path: str, hass: HomeAssistantType, vol.Optional('gravatar', default=None): vol.Any(None, cv.string), vol.Optional('picture', default=None): vol.Any(None, cv.string), vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( - cv.time_period, cv.positive_timedelta) + cv.time_period, cv.positive_timedelta), + vol.Optional('vendor', default=None): vol.Any(None, cv.string), }) try: result = [] @@ -546,7 +602,8 @@ def update_config(path: str, dev_id: str, device: Device): 'mac': device.mac, 'picture': device.config_picture, 'track': device.track, - CONF_AWAY_HIDE: device.away_hide + CONF_AWAY_HIDE: device.away_hide, + 'vendor': device.vendor, }} out.write('\n') out.write(dump(device)) diff --git a/tests/components/device_tracker/test_ddwrt.py b/tests/components/device_tracker/test_ddwrt.py index 5e0a90d3bbe..e86432e1659 100644 --- a/tests/components/device_tracker/test_ddwrt.py +++ b/tests/components/device_tracker/test_ddwrt.py @@ -3,6 +3,7 @@ import os import unittest from unittest import mock import logging +import re import requests import requests_mock @@ -17,6 +18,8 @@ from homeassistant.util import slugify from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture) +from ...test_util.aiohttp import mock_aiohttp_client + TEST_HOST = '127.0.0.1' _LOGGER = logging.getLogger(__name__) @@ -26,6 +29,13 @@ class TestDdwrt(unittest.TestCase): hass = None + def run(self, result=None): + """Mock out http calls to macvendor API for whole test suite.""" + with mock_aiohttp_client() as aioclient_mock: + macvendor_re = re.compile('http://api.macvendors.com/.*') + aioclient_mock.get(macvendor_re, text='') + super().run(result) + def setup_method(self, _): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -136,6 +146,7 @@ class TestDdwrt(unittest.TestCase): CONF_USERNAME: 'fake_user', CONF_PASSWORD: '0' }}) + self.hass.block_till_done() path = self.hass.config.path(device_tracker.YAML_DEVICES) devices = config.load_yaml_config_file(path) @@ -164,6 +175,7 @@ class TestDdwrt(unittest.TestCase): CONF_USERNAME: 'fake_user', CONF_PASSWORD: '0' }}) + self.hass.block_till_done() path = self.hass.config.path(device_tracker.YAML_DEVICES) devices = config.load_yaml_config_file(path) @@ -192,6 +204,7 @@ class TestDdwrt(unittest.TestCase): CONF_USERNAME: 'fake_user', CONF_PASSWORD: '0' }}) + self.hass.block_till_done() path = self.hass.config.path(device_tracker.YAML_DEVICES) devices = config.load_yaml_config_file(path) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index aa158cd8de6..e2ee21ab90d 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,5 +1,6 @@ """The tests for the device tracker component.""" # pylint: disable=protected-access +import asyncio import json import logging import unittest @@ -23,6 +24,8 @@ from tests.common import ( get_test_home_assistant, fire_time_changed, fire_service_discovered, patch_yaml_files, assert_setup_component) +from ...test_util.aiohttp import mock_aiohttp_client + TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}} _LOGGER = logging.getLogger(__name__) @@ -107,6 +110,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.config_picture, config.config_picture) self.assertEqual(device.away_hide, config.away_hide) self.assertEqual(device.consider_home, config.consider_home) + self.assertEqual(device.vendor, config.vendor) # pylint: disable=invalid-name @patch('homeassistant.components.device_tracker._LOGGER.warning') @@ -154,8 +158,13 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + + # wait for async calls (macvendor) to finish + self.hass.block_till_done() + config = device_tracker.load_config(self.yaml_devices, self.hass, timedelta(seconds=0)) + assert len(config) == 1 assert config[0].dev_id == 'dev1' assert config[0].track @@ -181,6 +190,72 @@ class TestComponentsDeviceTracker(unittest.TestCase): "55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar") self.assertEqual(device.config_picture, gravatar_url) + def test_mac_vendor_lookup(self): + """Test if vendor string is lookup on macvendors API.""" + mac = 'B8:27:EB:00:00:00' + vendor_string = 'Raspberry Pi Foundation' + + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') + + with mock_aiohttp_client() as aioclient_mock: + aioclient_mock.get('http://api.macvendors.com/b8:27:eb', + text=vendor_string) + + run_coroutine_threadsafe(device.set_vendor_for_mac(), + self.hass.loop).result() + assert aioclient_mock.call_count == 1 + + self.assertEqual(device.vendor, vendor_string) + + def test_mac_vendor_lookup_unknown(self): + """Prevent another mac vendor lookup if was not found first time.""" + mac = 'B8:27:EB:00:00:00' + + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') + + with mock_aiohttp_client() as aioclient_mock: + aioclient_mock.get('http://api.macvendors.com/b8:27:eb', + status=404) + + run_coroutine_threadsafe(device.set_vendor_for_mac(), + self.hass.loop).result() + + self.assertEqual(device.vendor, 'unknown') + + def test_mac_vendor_lookup_error(self): + """Prevent another lookup if failure during API call.""" + mac = 'B8:27:EB:00:00:00' + + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') + + with mock_aiohttp_client() as aioclient_mock: + aioclient_mock.get('http://api.macvendors.com/b8:27:eb', + status=500) + + run_coroutine_threadsafe(device.set_vendor_for_mac(), + self.hass.loop).result() + + self.assertEqual(device.vendor, 'unknown') + + def test_mac_vendor_lookup_exception(self): + """Prevent another lookup if exception during API call.""" + mac = 'B8:27:EB:00:00:00' + + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') + + with mock_aiohttp_client() as aioclient_mock: + aioclient_mock.get('http://api.macvendors.com/b8:27:eb', + exc=asyncio.TimeoutError()) + + run_coroutine_threadsafe(device.set_vendor_for_mac(), + self.hass.loop).result() + + self.assertEqual(device.vendor, 'unknown') + def test_discovery(self): """Test discovery.""" scanner = get_component('device_tracker.test').SCANNER diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index bbaf62a8680..d6f0c80b435 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -22,7 +22,8 @@ class AiohttpClientMocker: text=None, content=None, json=None, - params=None): + params=None, + exc=None): """Mock a request.""" if json: text = _json.dumps(json) @@ -33,6 +34,8 @@ class AiohttpClientMocker: if params: url = str(yarl.URL(url).with_query(params)) + self.exc = exc + self._mocks.append(AiohttpClientMockResponse( method, url, status, content)) @@ -68,6 +71,9 @@ class AiohttpClientMocker: for response in self._mocks: if response.match_request(method, url, params): self.mock_calls.append((method, url)) + + if self.exc: + raise self.exc return response assert False, "No mock registered for {} {}".format(method.upper(), From 8a042586f1f446ccf4e4779e49783cf94eba3fb7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 2 Dec 2016 03:31:55 +0100 Subject: [PATCH 114/137] Migrate sensor to async (#4663) --- homeassistant/components/sensor/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index c018c04cdaf..b4a467e240f 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -4,6 +4,7 @@ Component to interface with various sensors that can be monitored. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor/ """ +import asyncio import logging from homeassistant.helpers.entity_component import EntityComponent @@ -15,11 +16,11 @@ SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track states and offer events for sensors.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - component.setup(config) - + yield from component.async_setup(config) return True From 49cfe38ccabaf059049b292ae64f86bddfbe1a96 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Dec 2016 21:10:04 -0800 Subject: [PATCH 115/137] Demo platform to group climate instead of thermostat --- homeassistant/components/demo.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 3f3454e0f02..80f89d7c134 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -86,16 +86,11 @@ def setup(hass, config): group.Group.create_group(hass, 'people', [ 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', 'device_tracker.demo_paulus']) - group.Group.create_group(hass, 'thermostats', [ - 'thermostat.nest', 'thermostat.thermostat']) group.Group.create_group(hass, 'downstairs', [ 'group.living_room', 'group.kitchen', 'scene.romantic_lights', 'rollershutter.kitchen_window', 'rollershutter.living_room_window', 'group.doors', - 'thermostat.nest', - ], view=True) - group.Group.create_group(hass, 'Upstairs', [ - 'thermostat.thermostat', 'group.bedroom', + 'thermostat.ecobee', ], view=True) # Setup scripts From 2e6a48ff5f4936f4a93d0937c25ab760c2c02b8d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 2 Dec 2016 06:38:12 +0100 Subject: [PATCH 116/137] WIP: Migrate scene to async + homeassistant scene async (#4665) * Migrate scene to async + homeassistant scene async * fix lint * Update state.py * Fix tests --- homeassistant/components/scene/__init__.py | 34 +++++++++++++------ .../components/scene/homeassistant.py | 22 +++++++----- homeassistant/helpers/state.py | 14 ++++++-- tests/components/scene/__init__.py | 1 + .../{test_scene.py => scene/test_init.py} | 0 5 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 tests/components/scene/__init__.py rename tests/components/{test_scene.py => scene/test_init.py} (100%) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 5ac7a2d9c86..7934f4b610b 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -4,6 +4,7 @@ Allow users to set and activate scenes. For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ +import asyncio import logging from collections import namedtuple @@ -39,7 +40,8 @@ def activate(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_TURN_ON, data) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup scenes.""" logger = logging.getLogger(__name__) @@ -59,17 +61,21 @@ def setup(hass, config): component = EntityComponent(logger, DOMAIN, hass) - component.setup(config) + yield from component.async_setup(config) - def handle_scene_service(service): + @asyncio.coroutine + def async_handle_scene_service(service): """Handle calls to the switch services.""" - target_scenes = component.extract_from_service(service) + target_scenes = component.async_extract_from_service(service) + print(target_scenes) + print(component.entities) + tasks = [scene.async_activate() for scene in target_scenes] + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) - for scene in target_scenes: - scene.activate() - - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_scene_service, - schema=SCENE_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_scene_service, + schema=SCENE_SERVICE_SCHEMA) return True @@ -89,4 +95,12 @@ class Scene(Entity): def activate(self): """Activate scene. Try to get entities into requested state.""" - raise NotImplementedError + raise NotImplementedError() + + @asyncio.coroutine + def async_activate(self): + """Activate scene. Try to get entities into requested state. + + This method is a coroutine. + """ + yield from self.hass.loop.run_in_executor(None, self.activate) diff --git a/homeassistant/components/scene/homeassistant.py b/homeassistant/components/scene/homeassistant.py index e507c664bef..c7365ea65d9 100644 --- a/homeassistant/components/scene/homeassistant.py +++ b/homeassistant/components/scene/homeassistant.py @@ -4,13 +4,14 @@ Allow users to set and activate scenes. For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ +import asyncio from collections import namedtuple from homeassistant.components.scene import Scene from homeassistant.const import ( ATTR_ENTITY_ID, STATE_OFF, STATE_ON) from homeassistant.core import State -from homeassistant.helpers.state import reproduce_state +from homeassistant.helpers.state import async_reproduce_state DEPENDENCIES = ['group'] STATE = 'scening' @@ -20,21 +21,24 @@ CONF_ENTITIES = "entities" SceneConfig = namedtuple('SceneConfig', ['name', 'states']) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup home assistant scene entries.""" scene_config = config.get("states") if not isinstance(scene_config, list): scene_config = [scene_config] - add_devices(HomeAssistantScene(hass, _process_config(scene)) - for scene in scene_config) - + yield from async_add_devices(HomeAssistantScene( + hass, _process_config(scene)) for scene in scene_config) return True def _process_config(scene_config): - """Process passed in config into a format to work with.""" + """Process passed in config into a format to work with. + + Async friendly. + """ name = scene_config.get('name') states = {} @@ -81,6 +85,8 @@ class HomeAssistantScene(Scene): ATTR_ENTITY_ID: list(self.scene_config.states.keys()), } - def activate(self): + @asyncio.coroutine + def async_activate(self): """Activate scene. Try to get entities into requested state.""" - reproduce_state(self.hass, self.scene_config.states.values(), True) + yield from async_reproduce_state( + self.hass, self.scene_config.states.values(), True) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 10364eff815..9980ad11a8d 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,4 +1,5 @@ """Helpers that help with state related things.""" +import asyncio import json import logging from collections import defaultdict @@ -33,6 +34,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_UNLOCKED) from homeassistant.core import State +from homeassistant.util.async import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -111,6 +113,13 @@ def get_changed_since(states, utc_point_in_time): def reproduce_state(hass, states, blocking=False): + """Reproduce given state.""" + return run_coroutine_threadsafe( + async_reproduce_state(hass, states, blocking), hass.loop).result() + + +@asyncio.coroutine +def async_reproduce_state(hass, states, blocking=False): """Reproduce given state.""" if isinstance(states, State): states = [states] @@ -129,7 +138,7 @@ def reproduce_state(hass, states, blocking=False): else: service_domain = state.domain - domain_services = hass.services.services[service_domain] + domain_services = hass.services.async_services()[service_domain] service = None for _service in domain_services.keys(): @@ -157,7 +166,8 @@ def reproduce_state(hass, states, blocking=False): for (service_domain, service, service_data), entity_ids in to_call.items(): data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids - hass.services.call(service_domain, service, data, blocking) + yield from hass.services.async_call( + service_domain, service, data, blocking) def state_as_number(state): diff --git a/tests/components/scene/__init__.py b/tests/components/scene/__init__.py new file mode 100644 index 00000000000..6491c2ef020 --- /dev/null +++ b/tests/components/scene/__init__.py @@ -0,0 +1 @@ +"""Tests for scene component.""" diff --git a/tests/components/test_scene.py b/tests/components/scene/test_init.py similarity index 100% rename from tests/components/test_scene.py rename to tests/components/scene/test_init.py From 443553ff16087a0bce5089fea7d7f5c3fc416feb Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Thu, 1 Dec 2016 21:43:33 -0800 Subject: [PATCH 117/137] Handle IPv6 in zeroconf (#4052) --- homeassistant/components/zeroconf.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeroconf.py b/homeassistant/components/zeroconf.py index dca7baa997a..ab4a3b497fe 100644 --- a/homeassistant/components/zeroconf.py +++ b/homeassistant/components/zeroconf.py @@ -40,9 +40,16 @@ def setup(hass, config): 'requires_api_password': requires_api_password, } - info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, - socket.inet_aton(hass.config.api.host), - hass.config.api.port, 0, 0, params) + try: + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, + socket.inet_pton( + socket.AF_INET, hass.config.api.host), + hass.config.api.port, 0, 0, params) + except socket.error: + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, + socket.inet_pton( + socket.AF_INET6, hass.config.api.host), + hass.config.api.port, 0, 0, params) zeroconf.register_service(info) From 51e20c92f959047cc5d05742d4addc8a4fff58f8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 2 Dec 2016 06:45:19 +0100 Subject: [PATCH 118/137] WIP Fix pylint and PEP257 issues (tests) (#4120) * Fix pylint and PEP257 issues * More PEP257 fixes --- tests/components/automation/test_event.py | 5 +-- tests/components/automation/test_init.py | 35 +++++++++---------- tests/components/automation/test_mqtt.py | 6 ++-- .../automation/test_numeric_state.py | 7 ++-- tests/components/automation/test_state.py | 15 ++++---- tests/components/automation/test_sun.py | 7 ++-- tests/components/automation/test_template.py | 7 ++-- tests/components/automation/test_time.py | 6 ++-- tests/components/automation/test_zone.py | 12 ++++--- tests/components/binary_sensor/test_nx584.py | 2 ++ tests/components/camera/test_uvc.py | 6 ++-- tests/components/climate/test_demo.py | 11 +++--- tests/components/media_player/test_demo.py | 1 - tests/components/media_player/test_sonos.py | 16 ++++++--- tests/components/mqtt/test_init.py | 1 + tests/components/switch/test_rfxtrx.py | 4 ++- tests/components/test_conversation.py | 2 ++ tests/components/test_sleepiq.py | 1 + tests/components/test_sun.py | 3 +- 19 files changed, 89 insertions(+), 58 deletions(-) diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 22158402ff9..a81e4200f48 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -8,10 +8,11 @@ import homeassistant.components.automation as automation from tests.common import get_test_home_assistant +# pylint: disable=invalid-name class TestAutomationEvent(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -24,7 +25,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """"Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index e06984e9f7d..39df4af719d 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -14,11 +14,10 @@ from tests.common import get_test_home_assistant, assert_setup_component, \ fire_time_changed +# pylint: disable=invalid-name class TestAutomation(unittest.TestCase): """Test the event automation.""" - # pylint: disable=invalid-name - def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -27,12 +26,12 @@ class TestAutomation(unittest.TestCase): @callback def record_call(service): - """Record call.""" + """Helper to record calls.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() @@ -438,20 +437,20 @@ class TestAutomation(unittest.TestCase): @patch('homeassistant.config.load_yaml_config_file', autospec=True, return_value={ - automation.DOMAIN: { - 'alias': 'bye', - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event2', - }, - 'action': { - 'service': 'test.automation', - 'data_template': { - 'event': '{{ trigger.event.event_type }}' - } - } - } - }) + automation.DOMAIN: { + 'alias': 'bye', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event2', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } + } + }) def test_reload_config_service(self, mock_load_yaml): """Test the reload config service.""" assert setup_component(self.hass, automation.DOMAIN, { diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 4e58dc7a442..e704b9b2d64 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -8,10 +8,11 @@ from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) +# pylint: disable=invalid-name class TestAutomationMQTT(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -20,11 +21,12 @@ class TestAutomationMQTT(unittest.TestCase): @callback def record_call(service): + """Helper to record calls.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index d0aedd87f46..c7db55726eb 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -8,10 +8,11 @@ import homeassistant.components.automation as automation from tests.common import get_test_home_assistant +# pylint: disable=invalid-name class TestAutomationNumericState(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -451,8 +452,8 @@ class TestAutomationNumericState(unittest.TestCase): 'service': 'test.automation', 'data_template': { 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( - 'platform', 'entity_id', 'below', 'above', - 'from_state.state', 'to_state.state')) + 'platform', 'entity_id', 'below', 'above', + 'from_state.state', 'to_state.state')) }, } } diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 3b4e4486112..3f54c876c5f 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -1,6 +1,7 @@ """The test for state automation.""" -import unittest from datetime import timedelta + +import unittest from unittest.mock import patch from homeassistant.core import callback @@ -12,10 +13,11 @@ from tests.common import ( fire_time_changed, get_test_home_assistant, assert_setup_component) +# pylint: disable=invalid-name class TestAutomationState(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -24,11 +26,12 @@ class TestAutomationState(unittest.TestCase): @callback def record_call(service): + """Call recorder.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() @@ -47,9 +50,9 @@ class TestAutomationState(unittest.TestCase): 'service': 'test.automation', 'data_template': { 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( - 'platform', 'entity_id', - 'from_state.state', 'to_state.state', - 'for')) + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'for')) }, } } diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 475a8f55259..582533d8476 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -1,5 +1,6 @@ """The tests for the sun automation.""" from datetime import datetime + import unittest from unittest.mock import patch @@ -12,10 +13,11 @@ import homeassistant.util.dt as dt_util from tests.common import fire_time_changed, get_test_home_assistant +# pylint: disable=invalid-name class TestAutomationSun(unittest.TestCase): """Test the sun automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -25,11 +27,12 @@ class TestAutomationSun(unittest.TestCase): @callback def record_call(service): + """Call recorder.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index 1430d303140..7de2954a270 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -8,10 +8,11 @@ import homeassistant.components.automation as automation from tests.common import get_test_home_assistant, assert_setup_component +# pylint: disable=invalid-name class TestAutomationTemplate(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -20,12 +21,12 @@ class TestAutomationTemplate(unittest.TestCase): @callback def record_call(service): - """helper for recording calls.""" + """Helper to record calls.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index ff2d20145d9..10e7910adcb 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -12,10 +12,11 @@ from tests.common import ( fire_time_changed, get_test_home_assistant, assert_setup_component) +# pylint: disable=invalid-name class TestAutomationTime(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -23,11 +24,12 @@ class TestAutomationTime(unittest.TestCase): @callback def record_call(service): + """Helper to record calls.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index d81cb8f0bd5..72a79faa828 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -8,10 +8,11 @@ from homeassistant.components import automation, zone from tests.common import get_test_home_assistant +# pylint: disable=invalid-name class TestAutomationZone(unittest.TestCase): """Test the event automation.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') @@ -28,11 +29,12 @@ class TestAutomationZone(unittest.TestCase): @callback def record_call(service): + """Helper to record calls.""" self.calls.append(service) self.hass.services.register('test', 'automation', record_call) - def tearDown(self): # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() @@ -56,9 +58,9 @@ class TestAutomationZone(unittest.TestCase): 'service': 'test.automation', 'data_template': { 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( - 'platform', 'entity_id', - 'from_state.state', 'to_state.state', - 'zone.name')) + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) }, } diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index 6ed2ae476f3..5481bbc9198 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -179,6 +179,7 @@ class TestNX584Watcher(unittest.TestCase): @mock.patch.object(watcher, '_process_zone_event') def run(fake_process): + """Run a fake process.""" fake_process.side_effect = StopMe self.assertRaises(StopMe, watcher._run) self.assertEqual(fake_process.call_count, 1) @@ -193,6 +194,7 @@ class TestNX584Watcher(unittest.TestCase): empty_me = [1, 2] def fake_run(): + """Fake runner.""" if empty_me: empty_me.pop() raise requests.exceptions.ConnectionError() diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 41b272c15eb..f5cb0992ec0 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -121,7 +121,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_incomplete_config(self, mock_uvc): - """"Test the setup with incomplete configuration.""" + """Test the setup with incomplete configuration.""" assert setup_component( self.hass, 'camera', {'platform': 'uvc', 'nvr': 'foo'}) assert not mock_uvc.called @@ -135,7 +135,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch.object(uvc, 'UnifiVideoCamera') @mock.patch('uvcclient.nvr.UVCRemote') def test_setup_nvr_errors(self, mock_remote, mock_uvc): - """"Test for NVR errors.""" + """Test for NVR errors.""" errors = [nvr.NotAuthorized, nvr.NvrError, requests.exceptions.ConnectionError] config = { @@ -223,6 +223,7 @@ class TestUVC(unittest.TestCase): responses = [0] def fake_login(*a): + """Fake login.""" try: responses.pop(0) raise socket.error @@ -277,6 +278,7 @@ class TestUVC(unittest.TestCase): responses = [0] def fake_snapshot(): + """Fake snapshot.""" try: responses.pop() raise camera.CameraAuthError() diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 04fc2e33247..518e4ca2c81 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -32,7 +32,7 @@ class TestDemoClimate(unittest.TestCase): self.hass.stop() def test_setup_params(self): - """Test the inititial parameters.""" + """Test the initial parameters.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) self.assertEqual('on', state.attributes.get('away_mode')) @@ -93,8 +93,7 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(25.0, state.attributes.get('target_temp_high')) def test_set_target_temp_range_bad_attr(self): - """Test setting the target temperature range without required - attribute.""" + """Test setting the target temperature range without attribute.""" state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(21.0, state.attributes.get('target_temp_low')) @@ -163,8 +162,10 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual("Auto", state.attributes.get('swing_mode')) def test_set_operation_bad_attr_and_state(self): - """Test setting operation mode without required attribute, and - check the state.""" + """Test setting operation mode without required attribute. + + Also check the state. + """ state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index c9fb3ad6ff8..4da1fb6a725 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -34,7 +34,6 @@ class TestDemoMediaPlayer(unittest.TestCase): def test_source_select(self): """Test the input source service.""" - entity_id = 'media_player.lounge_room' assert setup_component( diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index b170f14c372..5c2911b2235 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -22,12 +22,17 @@ class socoDiscoverMock(): class AvTransportMock(): """Mock class for the avTransport property on soco.SoCo object.""" + def __init__(self): + """Initialize ethe Transport mock.""" pass def GetMediaInfo(self, _): - return {'CurrentURI': '', - 'CurrentURIMetaData': ''} + """Get the media details.""" + return { + 'CurrentURI': '', + 'CurrentURIMetaData': '' + } class SoCoMock(): @@ -102,18 +107,21 @@ def fake_add_device(devices, update_befor_add=False): class TestSonosMediaPlayer(unittest.TestCase): """Test the media_player module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() def monkey_available(self): + """Make a monkey available.""" return True # Monkey patches self.real_available = sonos.SonosDevice.available sonos.SonosDevice.available = monkey_available - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" # Monkey patches sonos.SonosDevice.available = self.real_available diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 01eb81b261e..ff74c070d85 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -325,6 +325,7 @@ class TestMQTTCallbacks(unittest.TestCase): self.assertEqual({}, mqtt.MQTT_CLIENT.progress) def test_invalid_mqtt_topics(self): + """Test invalid topics.""" self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic') self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one') diff --git a/tests/components/switch/test_rfxtrx.py b/tests/components/switch/test_rfxtrx.py index f0d38ca20c3..ddee7cccaf7 100644 --- a/tests/components/switch/test_rfxtrx.py +++ b/tests/components/switch/test_rfxtrx.py @@ -49,6 +49,7 @@ class TestSwitchRfxtrx(unittest.TestCase): }}})) def test_invalid_config1(self): + """Test invalid configuration.""" self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, @@ -60,7 +61,7 @@ class TestSwitchRfxtrx(unittest.TestCase): }}})) def test_invalid_config2(self): - """Test configuration.""" + """Test invalid configuration.""" self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, @@ -73,6 +74,7 @@ class TestSwitchRfxtrx(unittest.TestCase): }}})) def test_invalid_config3(self): + """Test invalid configuration.""" self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 919c95be4c5..abe3a8f36f1 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -41,6 +41,7 @@ class TestConversation(unittest.TestCase): @callback def record_call(service): + """Recorder for a call.""" calls.append(service) self.hass.services.register('light', 'turn_on', record_call) @@ -60,6 +61,7 @@ class TestConversation(unittest.TestCase): @callback def record_call(service): + """Recorder for a call.""" calls.append(service) self.hass.services.register('light', 'turn_off', record_call) diff --git a/tests/components/test_sleepiq.py b/tests/components/test_sleepiq.py index 5bdfba4163d..965e0b6304a 100644 --- a/tests/components/test_sleepiq.py +++ b/tests/components/test_sleepiq.py @@ -9,6 +9,7 @@ from tests.common import load_fixture, get_test_home_assistant def mock_responses(mock): + """Mock responses for SleepIQ.""" base_url = 'https://api.sleepiq.sleepnumber.com/rest/' mock.put( base_url + 'login', diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 15b79465b8f..9e5b15e6c2f 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -12,15 +12,14 @@ import homeassistant.components.sun as sun from tests.common import get_test_home_assistant +# pylint: disable=invalid-name class TestSun(unittest.TestCase): """Test the sun module.""" - # pylint: disable=invalid-name def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - # pylint: disable=invalid-name def tearDown(self): """Stop everything that was started.""" self.hass.stop() From 801a69be3ae6c04237a436efb78f9e243c3f6ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Gonz=C3=A1lez=20Calleja?= Date: Fri, 2 Dec 2016 00:00:17 -0600 Subject: [PATCH 119/137] Extending efergy component for get the amount of energy consumed (#4202) * Extending efergy component for get the amount of energy consumed * Changing units from kW to kWh * Chaning units for Instant Consumption from kWh to kW * Adding timeout for get and removing pylint config * Update efergy.py --- homeassistant/components/sensor/efergy.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/efergy.py b/homeassistant/components/sensor/efergy.py index 1fe5e7217de..f930bc9c23b 100644 --- a/homeassistant/components/sensor/efergy.py +++ b/homeassistant/components/sensor/efergy.py @@ -25,17 +25,18 @@ CONF_CURRENCY = 'currency' CONF_PERIOD = 'period' CONF_INSTANT = 'instant_readings' +CONF_AMOUNT = 'amount' CONF_BUDGET = 'budget' CONF_COST = 'cost' SENSOR_TYPES = { CONF_INSTANT: ['Energy Usage', 'kW'], + CONF_AMOUNT: ['Energy Consumed', 'kWh'], CONF_BUDGET: ['Energy Budget', None], CONF_COST: ['Energy Cost', None], } -TYPES_SCHEMA = vol.In( - [CONF_INSTANT, CONF_BUDGET, CONF_COST]) +TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema({ vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, @@ -100,17 +101,23 @@ class EfergySensor(Entity): try: if self.type == 'instant_readings': url_string = _RESOURCE + 'getInstant?token=' + self.app_token - response = get(url_string) + response = get(url_string, timeout=10) self._state = response.json()['reading'] / 1000 + elif self.type == 'amount': + url_string = _RESOURCE + 'getEnergy?token=' + self.app_token \ + + '&offset=' + self.utc_offset + '&period=' \ + + self.period + response = get(url_string, timeout=10) + self._state = response.json()['sum'] elif self.type == 'budget': url_string = _RESOURCE + 'getBudget?token=' + self.app_token - response = get(url_string) + response = get(url_string, timeout=10) self._state = response.json()['status'] elif self.type == 'cost': url_string = _RESOURCE + 'getCost?token=' + self.app_token \ + '&offset=' + self.utc_offset + '&period=' \ + self.period - response = get(url_string) + response = get(url_string, timeout=10) self._state = response.json()['sum'] else: self._state = 'Unknown' From ec8969351d95e9730440fea8f1600d1f0cd65126 Mon Sep 17 00:00:00 2001 From: Nick Touran Date: Thu, 1 Dec 2016 22:06:23 -0800 Subject: [PATCH 120/137] Prevent Pandora component from crashing or hanging during shutdown. (#4255) * Prevent Pandora component from crashing or hanging during shutdown. * Update pandora.py * Update pandora.py --- homeassistant/components/media_player/pandora.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index c97b20ee4bd..3d42e4a11e1 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -120,7 +120,7 @@ class PandoraMediaPlayer(MediaPlayerDevice): self.update_playing_status() self._player_state = STATE_IDLE - self.update_ha_state() + self.schedule_update_ha_state() def turn_off(self): """Turn the media player off.""" @@ -138,24 +138,24 @@ class PandoraMediaPlayer(MediaPlayerDevice): _LOGGER.info('Killed Pianobar subprocess') self._pianobar = None self._player_state = STATE_OFF - self.update_ha_state() + self.schedule_update_ha_state() def media_play(self): """Send play command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._player_state = STATE_PLAYING - self.update_ha_state() + self.schedule_update_ha_state() def media_pause(self): """Send pause command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._player_state = STATE_PAUSED - self.update_ha_state() + self.schedule_update_ha_state() def media_next_track(self): """Go to next track.""" self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK) - self.update_ha_state() + self.schedule_update_ha_state() @property def supported_media_commands(self): @@ -350,6 +350,8 @@ class PandoraMediaPlayer(MediaPlayerDevice): pass except pexpect.exceptions.TIMEOUT: pass + except pexpect.exceptions.EOF: + pass def _pianobar_exists(): From 08909ed420a36276f0245b788ddba0546ff19c32 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Fri, 2 Dec 2016 06:13:55 +0000 Subject: [PATCH 121/137] (InfluxDB) Configuration for a default measurement value for events without a unit. (#4632) --- homeassistant/components/influxdb.py | 8 +++++- tests/components/test_influxdb.py | 43 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index d96fb8c384f..ebb4169f8e6 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DB_NAME = 'database' CONF_TAGS = 'tags' +CONF_DEFAULT_MEASUREMENT = 'default_measurement' DEFAULT_DATABASE = 'home_assistant' DEFAULT_HOST = 'localhost' @@ -40,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, vol.Optional(CONF_TAGS, default={}): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_WHITELIST, default=[]): @@ -65,6 +67,7 @@ def setup(hass, config): blacklist = conf.get(CONF_BLACKLIST) whitelist = conf.get(CONF_WHITELIST) tags = conf.get(CONF_TAGS) + default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) try: influx = InfluxDBClient( @@ -96,7 +99,10 @@ def setup(hass, config): measurement = state.attributes.get('unit_of_measurement') if measurement in (None, ''): - measurement = state.entity_id + if default_measurement: + measurement = default_measurement + else: + measurement = state.entity_id json_body = [ { diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index de90e86c0bf..1ddac909c63 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -256,3 +256,46 @@ class TestInfluxDB(unittest.TestCase): else: self.assertFalse(mock_client.return_value.write_points.called) mock_client.return_value.write_points.reset_mock() + + def test_event_listener_default_measurement(self, mock_client): + """Test the event listener with a default measurement.""" + config = { + 'influxdb': { + 'host': 'host', + 'username': 'user', + 'password': 'pass', + 'default_measurement': 'state', + 'blacklist': ['fake.blacklisted'] + } + } + assert setup_component(self.hass, influxdb.DOMAIN, config) + self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] + + for entity_id in ('ok', 'blacklisted'): + state = mock.MagicMock( + state=1, domain='fake', entity_id='fake.{}'.format(entity_id), + object_id=entity_id, attributes={}) + event = mock.MagicMock(data={'new_state': state}, time_fired=12345) + body = [{ + 'measurement': 'state', + 'tags': { + 'domain': 'fake', + 'entity_id': entity_id, + }, + 'time': 12345, + 'fields': { + 'value': 1, + }, + }] + self.handler_method(event) + if entity_id == 'ok': + self.assertEqual( + mock_client.return_value.write_points.call_count, 1 + ) + self.assertEqual( + mock_client.return_value.write_points.call_args, + mock.call(body) + ) + else: + self.assertFalse(mock_client.return_value.write_points.called) + mock_client.return_value.write_points.reset_mock() From b1fbada02d3eb3387137b8ebce417111af7f1e0b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 2 Dec 2016 07:15:48 +0100 Subject: [PATCH 122/137] Update throttle and add more attributes (#4644) --- homeassistant/components/sensor/waqi.py | 111 ++++++++++++++++-------- 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index d66810e7c5d..b893eeaf204 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -6,92 +6,129 @@ https://home-assistant.io/components/sensor.waqi/ """ import logging from datetime import timedelta -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers import config_validation as cv + import voluptuous as vol -REQUIREMENTS = ["pwaqi==1.3"] +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_TEMPERATURE, STATE_UNKNOWN) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pwaqi==1.3'] _LOGGER = logging.getLogger(__name__) +ATTR_DOMINENTPOL = 'dominentpol' +ATTR_HUMIDITY = 'humidity' +ATTR_NITROGEN_DIOXIDE = 'nitrogen_dioxide' +ATTR_OZONE = 'ozone' +ATTR_PARTICLE = 'particle' +ATTR_PRESSURE = 'pressure' +ATTR_TIME = 'time' +ATTRIBUTION = 'Data provided by the World Air Quality Index project' + +CONF_LOCATIONS = 'locations' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + SENSOR_TYPES = { - 'aqi': ['AQI', '0-300+', 'mdi:cloud'] + 'aqi': ['AQI', '0-300+', 'mdi:cloud'] } -ATTR_LOCATION = 'locations' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(ATTR_LOCATION): cv.ensure_list + vol.Required(CONF_LOCATIONS): cv.ensure_list }) -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) - def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the requested World Air Quality Index locations.""" - dev = [] + """Set up the requested World Air Quality Index locations.""" import pwaqi - # Iterate each module - for location_name in config[ATTR_LOCATION]: - _LOGGER.debug('Adding location %s', location_name) + + dev = [] + for location_name in config.get(CONF_LOCATIONS): station_ids = pwaqi.findStationCodesByCity(location_name) - _LOGGER.debug('I got the following stations: %s', station_ids) + _LOGGER.error('The following stations were returned: %s', station_ids) for station in station_ids: - dev.append(WaqiSensor(station)) + dev.append(WaqiSensor(WaqiData(station), station)) add_devices(dev) -# pylint: disable=too-few-public-methods class WaqiSensor(Entity): """Implementation of a WAQI sensor.""" - def __init__(self, station_id): + def __init__(self, data, station_id): """Initialize the sensor.""" + self.data = data self._station_id = station_id - self._state = None + self._details = None self.update() @property def name(self): """Return the name of the sensor.""" - if 'city' in self._data: - return "WAQI {}".format(self._data['city']['name']) - return "WAQI {}".format(self._station_id) + try: + return 'WAQI {}'.format(self._details['city']['name']) + except (KeyError, TypeError): + return 'WAQI {}'.format(self._station_id) @property def icon(self): """Icon to use in the frontend, if any.""" - return "mdi:cloud" + return 'mdi:cloud' @property def state(self): """Return the state of the device.""" - return self._state + if self._details is not None: + return self._details.get('aqi') + else: + return STATE_UNKNOWN @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return "AQI" + return 'AQI' @property def state_attributes(self): """Return the state attributes of the last update.""" - return { - "time": self._data.get('time', 'no data'), - "dominentpol": self._data.get('dominentpol', 'no data') - } + try: + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_TIME: self._details.get('time'), + ATTR_HUMIDITY: self._details['iaqi'][5]['cur'], + ATTR_PRESSURE: self._details['iaqi'][4]['cur'], + ATTR_TEMPERATURE: self._details['iaqi'][3]['cur'], + ATTR_OZONE: self._details['iaqi'][1]['cur'], + ATTR_PARTICLE: self._details['iaqi'][0]['cur'], + ATTR_NITROGEN_DIOXIDE: self._details['iaqi'][2]['cur'], + ATTR_DOMINENTPOL: self._details.get('dominentpol'), + } + except (IndexError, KeyError): + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + self._details = self.data.data + + +class WaqiData(object): + """Get the latest data and update the states.""" + + def __init__(self, station_id): + """Initialize the data object.""" + self._station_id = station_id + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the data from World Air Quality Index and updates the states.""" import pwaqi try: - self._data = pwaqi.getStationObservation(self._station_id) - - self._state = self._data.get('aqi', 'no data') - except KeyError: - _LOGGER.exception('Unable to fetch data from WAQI.') + self.data = pwaqi.getStationObservation(self._station_id) + except AttributeError: + _LOGGER.exception("Unable to fetch data from WAQI") From 1f5f4e7a8964a458413bf7fc35afd6a680aee9ab Mon Sep 17 00:00:00 2001 From: Matt N Date: Thu, 1 Dec 2016 22:17:38 -0800 Subject: [PATCH 123/137] zoneminder: Support excluding archived events (#4445) --- homeassistant/components/sensor/zoneminder.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index 32a77a8f32f..388c12641c5 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -6,17 +6,32 @@ https://home-assistant.io/components/sensor.zoneminder/ """ import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.entity import Entity import homeassistant.components.zoneminder as zoneminder +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zoneminder'] +CONF_INCLUDE_ARCHIVED = "include_archived" + +DEFAULT_INCLUDE_ARCHIVED = False + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_INCLUDE_ARCHIVED, default=DEFAULT_INCLUDE_ARCHIVED): + cv.boolean, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the ZoneMinder sensor platform.""" + include_archived = config.get(CONF_INCLUDE_ARCHIVED) + sensors = [] monitors = zoneminder.get_state('api/monitors.json') @@ -25,7 +40,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ZMSensorMonitors(int(i['Monitor']['Id']), i['Monitor']['Name']) ) sensors.append( - ZMSensorEvents(int(i['Monitor']['Id']), i['Monitor']['Name']) + ZMSensorEvents(int(i['Monitor']['Id']), i['Monitor']['Name'], + include_archived) ) add_devices(sensors) @@ -64,10 +80,11 @@ class ZMSensorMonitors(Entity): class ZMSensorEvents(Entity): """Get the number of events for each monitor.""" - def __init__(self, monitor_id, monitor_name): + def __init__(self, monitor_id, monitor_name, include_archived): """Initiate event sensor.""" self._monitor_id = monitor_id self._monitor_name = monitor_name + self._include_archived = include_archived self._state = None @property @@ -87,8 +104,13 @@ class ZMSensorEvents(Entity): def update(self): """Update the sensor.""" + archived_filter = '/Archived:0' + if self._include_archived: + archived_filter = '' + event = zoneminder.get_state( - 'api/events/index/MonitorId:%i.json' % self._monitor_id + 'api/events/index/MonitorId:%i%s.json' % (self._monitor_id, + archived_filter) ) self._state = event['pagination']['count'] From b0a800cc6de83722281979afc02480ae4ca427ce Mon Sep 17 00:00:00 2001 From: Alberto Arias Maestro Date: Thu, 1 Dec 2016 22:20:44 -0800 Subject: [PATCH 124/137] Update commands to match the strings in pynx584 (#4623) The command string don't match the ones pynx584. See source code: https://github.com/kk7ds/pynx584/blob/master/nx584/api.py#L68 --- homeassistant/components/alarm_control_panel/nx584.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 8e3b327aecb..cb32fc924e6 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -117,11 +117,11 @@ class NX584Alarm(alarm.AlarmControlPanel): def alarm_arm_home(self, code=None): """Send arm home command.""" - self._alarm.arm('home') + self._alarm.arm('stay') def alarm_arm_away(self, code=None): """Send arm away command.""" - self._alarm.arm('auto') + self._alarm.arm('exit') def alarm_trigger(self, code=None): """Alarm trigger command.""" From 83a108b20a29019a09724f0b072f310887358bfd Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Fri, 2 Dec 2016 06:22:03 +0000 Subject: [PATCH 125/137] Sonos specify IP for event subscription (#4177) --- .../components/media_player/sonos.py | 24 ++++- tests/components/media_player/test_sonos.py | 95 +++++++++++++++++-- 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 022e5742ee7..b5367486e38 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -15,9 +15,10 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID) + STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID, + CONF_HOSTS) from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -49,9 +50,18 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' SUPPORT_SOURCE_LINEIN = 'Line-in' SUPPORT_SOURCE_TV = 'TV' +CONF_ADVERTISE_ADDR = 'advertise_addr' +CONF_INTERFACE_ADDR = 'interface_addr' + # Service call validation schemas ATTR_SLEEP_TIME = 'sleep_time' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ADVERTISE_ADDR): cv.string, + vol.Optional(CONF_INTERFACE_ADDR): cv.string, + vol.Optional(CONF_HOSTS): cv.ensure_list(cv.string), +}) + SONOS_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, }) @@ -70,6 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import soco global DEVICES + advertise_addr = config.get(CONF_ADVERTISE_ADDR, None) + if advertise_addr: + soco.config.EVENT_ADVERTISE_IP = advertise_addr + if discovery_info: player = soco.SoCo(discovery_info) @@ -87,18 +101,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False players = None - hosts = config.get('hosts', None) + hosts = config.get(CONF_HOSTS, None) if hosts: # Support retro compatibility with comma separated list of hosts # from config + hosts = hosts[0] if len(hosts) == 1 else hosts hosts = hosts.split(',') if isinstance(hosts, str) else hosts players = [] for host in hosts: players.append(soco.SoCo(socket.gethostbyname(host))) if not players: - players = soco.discover(interface_addr=config.get('interface_addr', - None)) + players = soco.discover(interface_addr=config.get(CONF_INTERFACE_ADDR)) if not players: _LOGGER.warning('No Sonos speakers found.') diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 5c2911b2235..9835b8d7635 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -5,7 +5,11 @@ import soco.snapshot from unittest import mock import soco -from homeassistant.components.media_player import sonos +from homeassistant.bootstrap import setup_component +from homeassistant.components.media_player import sonos, DOMAIN +from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR, \ + CONF_ADVERTISE_ADDR +from homeassistant.const import CONF_HOSTS, CONF_PLATFORM from tests.common import get_test_home_assistant @@ -134,20 +138,95 @@ class TestSonosMediaPlayer(unittest.TestCase): """Test a single device using the autodiscovery provided by HASS.""" sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') - # Ensure registration took place (#2558) self.assertEqual(len(sonos.DEVICES), 1) self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_ensure_setup_config(self, *args): - """Test a single address config'd by the HASS config file.""" - sonos.setup_platform(self.hass, - {'hosts': '192.0.2.1'}, - fake_add_device) + @mock.patch('soco.discover') + def test_ensure_setup_config_interface_addr(self, discover_mock, *args): + """Test a interface address config'd by the HASS config file.""" + discover_mock.return_value = {SoCoMock('192.0.2.1')} + + config = { + DOMAIN: { + CONF_PLATFORM: 'sonos', + CONF_INTERFACE_ADDR: '192.0.1.1', + } + } + + assert setup_component(self.hass, DOMAIN, config) - # Ensure registration took place (#2558) self.assertEqual(len(sonos.DEVICES), 1) + self.assertEqual(discover_mock.call_count, 1) + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) + @mock.patch('soco.discover') + def test_ensure_setup_config_advertise_addr(self, discover_mock, + *args): + """Test a advertise address config'd by the HASS config file.""" + discover_mock.return_value = {SoCoMock('192.0.2.1')} + + config = { + DOMAIN: { + CONF_PLATFORM: 'sonos', + CONF_ADVERTISE_ADDR: '192.0.1.1', + } + } + + assert setup_component(self.hass, DOMAIN, config) + + self.assertEqual(len(sonos.DEVICES), 1) + self.assertEqual(discover_mock.call_count, 1) + self.assertEqual(soco.config.EVENT_ADVERTISE_IP, '192.0.1.1') + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_ensure_setup_config_hosts_string_single(self, *args): + """Test a single address config'd by the HASS config file.""" + config = { + DOMAIN: { + CONF_PLATFORM: 'sonos', + CONF_HOSTS: ['192.0.2.1'], + } + } + + assert setup_component(self.hass, DOMAIN, config) + + self.assertEqual(len(sonos.DEVICES), 1) + self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_ensure_setup_config_hosts_string_multiple(self, *args): + """Test multiple address string config'd by the HASS config file.""" + config = { + DOMAIN: { + CONF_PLATFORM: 'sonos', + CONF_HOSTS: ['192.0.2.1,192.168.2.2'], + } + } + + assert setup_component(self.hass, DOMAIN, config) + + self.assertEqual(len(sonos.DEVICES), 2) + self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_ensure_setup_config_hosts_list(self, *args): + """Test a multiple address list config'd by the HASS config file.""" + config = { + DOMAIN: { + CONF_PLATFORM: 'sonos', + CONF_HOSTS: ['192.0.2.1', '192.168.2.2'], + } + } + + assert setup_component(self.hass, DOMAIN, config) + + self.assertEqual(len(sonos.DEVICES), 2) self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') @mock.patch('soco.SoCo', new=SoCoMock) From 48fd8f1f6362839d217dfaf9f68e78b7240171b9 Mon Sep 17 00:00:00 2001 From: Brent Hughes Date: Fri, 2 Dec 2016 01:02:58 -0600 Subject: [PATCH 126/137] InfluxDB: Fixed attributes that are lists causing invalid syntax (#4642) * Fixed attributes that are lists cuasing invalid influx syntax * Added bool and fixed mixed data type issue * Fixed changing nearly all data types to float causing some worse influxdb errors. whoops * Added line to end of file --- homeassistant/components/influxdb.py | 6 +++- tests/components/test_influxdb.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index ebb4169f8e6..167767bc00e 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -120,7 +120,11 @@ def setup(hass, config): for key, value in state.attributes.items(): if key != 'unit_of_measurement': - json_body[0]['fields'][key] = value + if isinstance(value, (str, float, bool)): + json_body[0]['fields'][key] = value + elif isinstance(value, int): + # Prevent column data errors in influxDB. + json_body[0]['fields'][key] = float(value) json_body[0]['tags'].update(tags) diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index 1ddac909c63..f7536958283 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -257,6 +257,50 @@ class TestInfluxDB(unittest.TestCase): self.assertFalse(mock_client.return_value.write_points.called) mock_client.return_value.write_points.reset_mock() + def test_event_listener_invalid_type(self, mock_client): + """Test the event listener when an attirbute has an invalid type.""" + self._setup() + + valid = { + '1': 1, + '1.0': 1.0, + STATE_ON: 1, + STATE_OFF: 0, + 'foo': 'foo' + } + for in_, out in valid.items(): + attrs = { + 'unit_of_measurement': 'foobars', + 'longitude': '1.1', + 'latitude': '2.2', + 'invalid_attribute': ['value1', 'value2'] + } + state = mock.MagicMock( + state=in_, domain='fake', object_id='entity', attributes=attrs) + event = mock.MagicMock(data={'new_state': state}, time_fired=12345) + body = [{ + 'measurement': 'foobars', + 'tags': { + 'domain': 'fake', + 'entity_id': 'entity', + }, + 'time': 12345, + 'fields': { + 'value': out, + 'longitude': '1.1', + 'latitude': '2.2' + }, + }] + self.handler_method(event) + self.assertEqual( + mock_client.return_value.write_points.call_count, 1 + ) + self.assertEqual( + mock_client.return_value.write_points.call_args, + mock.call(body) + ) + mock_client.return_value.write_points.reset_mock() + def test_event_listener_default_measurement(self, mock_client): """Test the event listener with a default measurement.""" config = { From 84c89686a9270ac2dabd21806753dca3545372aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Dec 2016 09:13:39 -0800 Subject: [PATCH 127/137] Update __init__.py --- homeassistant/components/scene/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 7934f4b610b..3f532a33151 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -67,8 +67,7 @@ def async_setup(hass, config): def async_handle_scene_service(service): """Handle calls to the switch services.""" target_scenes = component.async_extract_from_service(service) - print(target_scenes) - print(component.entities) + tasks = [scene.async_activate() for scene in target_scenes] if tasks: yield from asyncio.wait(tasks, loop=hass.loop) From 4874030b70ea0464848e6b8195e336b23537082a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Dec 2016 18:17:46 -0800 Subject: [PATCH 128/137] Have api_streams sensor also monitor websocket connections (#4668) --- .../components/sensor/api_streams.py | 42 +++++++++++++++---- tests/components/sensor/test_api_streams.py | 34 +++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/api_streams.py b/homeassistant/components/sensor/api_streams.py index 232ff74e862..15cfc200c4d 100644 --- a/homeassistant/components/sensor/api_streams.py +++ b/homeassistant/components/sensor/api_streams.py @@ -2,9 +2,15 @@ import asyncio import logging +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback from homeassistant.helpers.entity import Entity +NAME_WS = 'homeassistant.components.websocket_api' +NAME_STREAM = 'homeassistant.components.api' + + class StreamHandler(logging.Handler): """Check log messages for stream connect/disconnect.""" @@ -16,13 +22,24 @@ class StreamHandler(logging.Handler): def handle(self, record): """Handle a log message.""" - if not record.msg.startswith('STREAM'): - return + if record.name == NAME_STREAM: + if not record.msg.startswith('STREAM'): + return - if record.msg.endswith('ATTACHED'): - self.entity.count += 1 - elif record.msg.endswith('RESPONSE CLOSED'): - self.entity.count -= 1 + if record.msg.endswith('ATTACHED'): + self.entity.count += 1 + elif record.msg.endswith('RESPONSE CLOSED'): + self.entity.count -= 1 + + else: + if not record.msg.startswith('WS'): + return + elif len(record.args) < 2: + return + elif record.args[1] == 'Connected': + self.entity.count += 1 + elif record.args[1] == 'Closed connection': + self.entity.count -= 1 self.entity.schedule_update_ha_state() @@ -31,9 +48,18 @@ class StreamHandler(logging.Handler): def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the logger for filters.""" entity = APICount() + handler = StreamHandler(entity) - logging.getLogger('homeassistant.components.api').addHandler( - StreamHandler(entity)) + logging.getLogger(NAME_STREAM).addHandler(handler) + logging.getLogger(NAME_WS).addHandler(handler) + + @callback + def remove_logger(event): + """Remove our handlers.""" + logging.getLogger(NAME_STREAM).removeHandler(handler) + logging.getLogger(NAME_WS).removeHandler(handler) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, remove_logger) yield from async_add_devices([entity]) diff --git a/tests/components/sensor/test_api_streams.py b/tests/components/sensor/test_api_streams.py index 2245a650a1a..2154cc3bd49 100644 --- a/tests/components/sensor/test_api_streams.py +++ b/tests/components/sensor/test_api_streams.py @@ -37,3 +37,37 @@ def test_api_streams(hass): state = hass.states.get('sensor.connected_clients') assert state.state == '1' + + +@asyncio.coroutine +def test_websocket_api(hass): + """Test API streams.""" + log = logging.getLogger('homeassistant.components.websocket_api') + + with assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', { + 'sensor': { + 'platform': 'api_streams', + } + }) + + state = hass.states.get('sensor.connected_clients') + assert state.state == '0' + + log.debug('WS %s: %s', id(log), 'Connected') + yield from hass.async_block_till_done() + + state = hass.states.get('sensor.connected_clients') + assert state.state == '1' + + log.debug('WS %s: %s', id(log), 'Connected') + yield from hass.async_block_till_done() + + state = hass.states.get('sensor.connected_clients') + assert state.state == '2' + + log.debug('WS %s: %s', id(log), 'Closed connection') + yield from hass.async_block_till_done() + + state = hass.states.get('sensor.connected_clients') + assert state.state == '1' From 754d98bcd5adc758fd1c280c40321e7c640cd779 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 3 Dec 2016 14:06:08 +0100 Subject: [PATCH 129/137] Cleanups on homematic climate (#4685) --- homeassistant/components/climate/homematic.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index d0d91ac7270..877447eb671 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -97,13 +97,9 @@ class HMThermostat(HMDevice, ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - if not self.available: + if not self.available or temperature is None: return None - if temperature is None: - return - if self.current_operation == STATE_AUTO: - return self._hmdevice.actionNodeData('MANU_MODE', temperature) self._hmdevice.set_temperature(temperature) def set_operation_mode(self, operation_mode): From 64a5bff5b299d25e5e0e25aebd0530efd9a4408d Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Sat, 3 Dec 2016 12:26:47 -0500 Subject: [PATCH 130/137] Nest further improvements (#4655) * Further improvements on nest platform - fix binary sensor - add deprecations for monitored_conditions - better names for sensors (includes device type) * lint * Remove unused weather sensor * Fix to python-nest to a specific commit * lint * lint * lint * lint --- .../components/binary_sensor/nest.py | 105 +++++++++++++++--- homeassistant/components/nest.py | 18 ++- homeassistant/components/sensor/nest.py | 74 ++++++------ requirements_all.txt | 2 +- 4 files changed, 143 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 65fe6041f34..d78e33c9f95 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -4,46 +4,97 @@ Support for Nest Thermostat Binary Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.nest/ """ +from itertools import chain +import logging + import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.sensor.nest import NestSensor from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) -from homeassistant.components.nest import DATA_NEST +from homeassistant.components.nest import ( + DATA_NEST, is_thermostat, is_camera) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['nest'] -BINARY_TYPES = ['fan', - 'hvac_ac_state', - 'hvac_aux_heater_state', - 'hvac_heater_state', - 'hvac_heat_x2_state', - 'hvac_heat_x3_state', - 'hvac_alt_heat_state', - 'hvac_alt_heat_x2_state', - 'hvac_emer_heat_state', - 'online'] + +BINARY_TYPES = ['online'] + +CLIMATE_BINARY_TYPES = ['fan', + 'is_using_emergency_heat', + 'is_locked', + 'has_leaf'] + +CAMERA_BINARY_TYPES = [ + 'motion_detected', + 'sound_detected', + 'person_detected'] + +_BINARY_TYPES_DEPRECATED = [ + 'hvac_ac_state', + 'hvac_aux_heater_state', + 'hvac_heater_state', + 'hvac_heat_x2_state', + 'hvac_heat_x3_state', + 'hvac_alt_heat_state', + 'hvac_alt_heat_x2_state', + 'hvac_emer_heat_state'] + +_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ + + CAMERA_BINARY_TYPES +_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED = _VALID_BINARY_SENSOR_TYPES \ + + _BINARY_TYPES_DEPRECATED + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(BINARY_TYPES)]), + vol.All(cv.ensure_list, + [vol.In(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)]) }) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Nest binary sensors.""" nest = hass.data[DATA_NEST] + conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES) - all_sensors = [] - for structure, device in nest.devices(): - all_sensors.extend( - [NestBinarySensor(structure, device, variable) - for variable in config[CONF_MONITORED_CONDITIONS]]) + for variable in conf: + if variable in _BINARY_TYPES_DEPRECATED: + wstr = (variable + " is no a longer supported " + "monitored_conditions. See " + "https://home-assistant.io/components/binary_sensor.nest/ " + "for valid options, or remove monitored_conditions " + "entirely to get a reasonable default") + _LOGGER.error(wstr) - add_devices(all_sensors, True) + sensors = [] + device_chain = chain(nest.devices(), + nest.protect_devices(), + nest.camera_devices()) + for structure, device in device_chain: + sensors += [NestBinarySensor(structure, device, variable) + for variable in conf + if variable in BINARY_TYPES] + sensors += [NestBinarySensor(structure, device, variable) + for variable in conf + if variable in CLIMATE_BINARY_TYPES + and is_thermostat(device)] + + if is_camera(device): + sensors += [NestBinarySensor(structure, device, variable) + for variable in conf + if variable in CAMERA_BINARY_TYPES] + for activity_zone in device.activity_zones: + sensors += [NestActivityZoneSensor(structure, + device, + activity_zone)] + + add_devices(sensors, True) class NestBinarySensor(NestSensor, BinarySensorDevice): @@ -57,3 +108,21 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): def update(self): """Retrieve latest state.""" self._state = bool(getattr(self.device, self.variable)) + + +class NestActivityZoneSensor(NestBinarySensor): + """Represents a Nest binary sensor for activity in a zone.""" + + def __init__(self, structure, device, zone): + """Initialize the sensor.""" + super(NestActivityZoneSensor, self).__init__(structure, device, None) + self.zone = zone + + @property + def name(self): + """Return the name of the nest, if any.""" + return "{} {} activity".format(self._name, self.zone.name) + + def update(self): + """Retrieve latest state.""" + self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 10310390dfe..a606cb488d4 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = [ 'git+https://github.com/technicalpickles/python-nest.git' - '@nest-cam' + '@0be5c8a6307ee81540f21aac4fcd22cc5d98c988' # nest-cam branch '#python-nest==3.0.0'] DOMAIN = 'nest' @@ -89,6 +89,7 @@ def setup_nest(hass, nest, config, pin=None): _LOGGER.debug("proceeding with discovery") discovery.load_platform(hass, 'climate', DOMAIN, {}, config) discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'camera', DOMAIN, {}, config) _LOGGER.debug("setup done") @@ -172,3 +173,18 @@ class NestDevice(object): except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") + + +def is_thermostat(device): + """Target devices that are Nest Thermostats.""" + return bool(device.__class__.__name__ == 'Device') + + +def is_protect(device): + """Target devices that are Nest Protect Smoke Alarms.""" + return bool(device.__class__.__name__ == 'ProtectDevice') + + +def is_camera(device): + """Target devices that are Nest Protect Smoke Alarms.""" + return bool(device.__class__.__name__ == 'CameraDevice') diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 1173fcedd57..b4909aebae3 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.nest/ """ from itertools import chain +import logging import voluptuous as vol @@ -17,11 +18,13 @@ from homeassistant.const import ( DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', - 'operation_mode', - 'last_connection'] + 'operation_mode'] -SENSOR_TYPES_DEPRECATED = ['battery_health', - 'last_ip', +SENSOR_TYPES_DEPRECATED = ['last_ip', + 'local_ip', + 'last_connection'] + +SENSOR_TYPES_DEPRECATED = ['last_ip', 'local_ip'] WEATHER_VARS = {} @@ -43,22 +46,48 @@ PROTECT_VARS_DEPRECATED = ['battery_level'] SENSOR_TEMP_TYPES = ['temperature', 'target'] -_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS + \ - list(WEATHER_VARS.keys()) +_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \ + + list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED + +_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS \ + + list(WEATHER_VARS.keys()) + +_VALID_SENSOR_TYPES_WITH_DEPRECATED = _VALID_SENSOR_TYPES \ + + _SENSOR_TYPES_DEPRECATED PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): DOMAIN, vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Required(CONF_MONITORED_CONDITIONS): [vol.In(_VALID_SENSOR_TYPES)], + vol.Required(CONF_MONITORED_CONDITIONS): + [vol.In(_VALID_SENSOR_TYPES_WITH_DEPRECATED)] }) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest Sensor.""" nest = hass.data[DATA_NEST] conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_SENSOR_TYPES) + for variable in conf: + if variable in _SENSOR_TYPES_DEPRECATED: + if variable in DEPRECATED_WEATHER_VARS: + wstr = ("Nest no longer provides weather data like %s. See " + "https://home-assistant.io/components/#weather " + "for a list of other weather components to use." % + variable) + else: + wstr = (variable + " is no a longer supported " + "monitored_conditions. See " + "https://home-assistant.io/components/" + "binary_sensor.nest/ " + "for valid options, or remove monitored_conditions " + "entirely to get a reasonable default") + + _LOGGER.error(wstr) + all_sensors = [] for structure, device in chain(nest.devices(), nest.protect_devices()): sensors = [NestBasicSensor(structure, device, variable) @@ -67,10 +96,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors += [NestTempSensor(structure, device, variable) for variable in conf if variable in SENSOR_TEMP_TYPES and is_thermostat(device)] - sensors += [NestWeatherSensor(structure, device, - WEATHER_VARS[variable]) - for variable in conf - if variable in WEATHER_VARS and is_thermostat(device)] sensors += [NestProtectSensor(structure, device, variable) for variable in conf if variable in PROTECT_VARS and is_protect(device)] @@ -100,13 +125,13 @@ class NestSensor(Entity): # device specific self._location = self.device.where - self._name = self.device.name + self._name = self.device.name_long self._state = None @property def name(self): """Return the name of the nest, if any.""" - return "{} {}".format(self._name, self.variable) + return "{} {}".format(self._name, self.variable.replace("_", " ")) class NestBasicSensor(NestSensor): @@ -159,29 +184,6 @@ class NestTempSensor(NestSensor): self._state = round(temp, 1) -class NestWeatherSensor(NestSensor): - """Representation a basic Nest Weather Conditions sensor.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Retrieve latest state.""" - if self.variable == 'kph' or self.variable == 'direction': - self._state = getattr(self.structure.weather.current.wind, - self.variable) - else: - self._state = getattr(self.structure.weather.current, - self.variable) - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return SENSOR_UNITS.get(self.variable, None) - - class NestProtectSensor(NestSensor): """Return the state of nest protect.""" diff --git a/requirements_all.txt b/requirements_all.txt index 0d3709509fb..82dce9a1d39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -131,7 +131,7 @@ fuzzywuzzy==0.14.0 # gattlib==0.20150805 # homeassistant.components.nest -git+https://github.com/technicalpickles/python-nest.git@nest-cam#python-nest==3.0.0 +git+https://github.com/technicalpickles/python-nest.git@0be5c8a6307ee81540f21aac4fcd22cc5d98c988#python-nest==3.0.0 # homeassistant.components.notify.gntp gntp==1.0.3 From 898ba56d9fe958ab09c9adfcba8082a497eeb494 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Dec 2016 09:49:10 -0800 Subject: [PATCH 131/137] Fix aiohttp build (#4691) --- requirements_all.txt | 3 ++- setup.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 82dce9a1d39..35fad590e86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,8 @@ pip>=7.0.0 jinja2>=2.8 voluptuous==0.9.2 typing>=3,<4 -aiohttp==1.1.5 +yarl==0.7.1 +aiohttp==1.1.6 async_timeout==1.1.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index 7060550c723..9bd1719d38b 100755 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ DOWNLOAD_URL = ('{}/archive/' PACKAGES = find_packages(exclude=['tests', 'tests.*']) +# Pinning yarl because yarl 0.8 breaks aiohttp REQUIRES = [ 'requests>=2,<3', 'pyyaml>=3.11,<4', @@ -21,7 +22,8 @@ REQUIRES = [ 'jinja2>=2.8', 'voluptuous==0.9.2', 'typing>=3,<4', - 'aiohttp==1.1.5', + 'yarl==0.7.1', + 'aiohttp==1.1.6', 'async_timeout==1.1.0', ] From f63a79ee8fb8b28854e15db94433c595977973aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Dec 2016 09:59:20 -0800 Subject: [PATCH 132/137] Remove not dev related scripts (#4690) --- script/get_entities.py | 98 --------------------------- script/hass-daemon | 103 ---------------------------- script/home-assistant@.service | 20 ------ script/nginx-hass | 120 --------------------------------- 4 files changed, 341 deletions(-) delete mode 100755 script/get_entities.py delete mode 100755 script/hass-daemon delete mode 100644 script/home-assistant@.service delete mode 100644 script/nginx-hass diff --git a/script/get_entities.py b/script/get_entities.py deleted file mode 100755 index c07bc92f749..00000000000 --- a/script/get_entities.py +++ /dev/null @@ -1,98 +0,0 @@ -#! /usr/bin/python -""" -Query the Home Assistant API for available entities. - -Output is printed to stdout. -""" - -import sys -import getpass -import argparse -try: - from urllib2 import urlopen - PYTHON = 2 -except ImportError: - from urllib.request import urlopen - PYTHON = 3 -import json - - -def main(password, askpass, attrs, address, port): - """Fetch Home Assistant API JSON page and post process.""" - # Ask for password - if askpass: - password = getpass.getpass('Home Assistant API Password: ') - - # Fetch API result - url = mk_url(address, port, password) - response = urlopen(url).read() - if PYTHON == 3: - response = response.decode('utf-8') - data = json.loads(response) - - # Parse data - output = {'entity_id': []} - output.update([(attr, []) for attr in attrs]) - for item in data: - output['entity_id'].append(item['entity_id']) - for attr in attrs: - output[attr].append(item['attributes'].get(attr, '')) - - # Output data - print_table(output, ['entity_id'] + attrs) - - -def print_table(data, columns): - """Format and print a table of data from a dictionary.""" - # Get column lengths - lengths = {} - for key, value in data.items(): - lengths[key] = max([len(str(val)) for val in value] + [len(key)]) - - # Print header - for item in columns: - itemup = item.upper() - sys.stdout.write(itemup + ' ' * (lengths[item] - len(item) + 4)) - sys.stdout.write('\n') - - # print body - for ind in range(len(data[columns[0]])): - for item in columns: - val = str(data[item][ind]) - sys.stdout.write(val + ' ' * (lengths[item] - len(val) + 4)) - sys.stdout.write("\n") - - -def mk_url(address, port, password): - """Construct the URL call for the API states page.""" - url = '' - if address.startswith('http://'): - url += address - else: - url += 'http://' + address - url += ':' + port + '/api/states?' - if password is not None: - url += 'api_password=' + password - return url - - -if __name__ == "__main__": - all_options = {'password': None, 'askpass': False, 'attrs': [], - 'address': 'localhost', 'port': '8123'} - - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('attrs', metavar='ATTRIBUTE', type=str, nargs='*', - help='an attribute to read from the state') - parser.add_argument('--password', dest='password', default=None, - type=str, help='API password for the HA server') - parser.add_argument('--ask-password', dest='askpass', default=False, - action='store_const', const=True, - help='prompt for HA API password') - parser.add_argument('--addr', dest='address', - default='localhost', type=str, - help='address of the HA server') - parser.add_argument('--port', dest='port', default='8123', - type=str, help='port that HA is hosting on') - - args = parser.parse_args() - main(args.password, args.askpass, args.attrs, args.address, args.port) diff --git a/script/hass-daemon b/script/hass-daemon deleted file mode 100755 index 0501ba885a2..00000000000 --- a/script/hass-daemon +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/sh -### BEGIN INIT INFO -# Provides: hass -# Required-Start: $local_fs $network $named $time $syslog -# Required-Stop: $local_fs $network $named $time $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Description: Home\ Assistant -### END INIT INFO - -# /etc/init.d Service Script for Home Assistant -# Created with: https://gist.github.com/naholyr/4275302#file-new-service-sh -# -# Installation: -# 1) If any commands need to run before executing hass (like loading a -# virutal environment), put them in PRE_EXEC. This command must end with -# a semicolon. -# 2) Set RUN_AS to the username that should be used to execute hass. -# 3) Copy this script to /etc/init.d/ -# sudo cp hass-daemon /etc/init.d/hass-daemon -# sudo chmod +x /etc/init.d/hass-daemon -# 4) Register the daemon with Linux -# sudo update-rc.d hass-daemon defaults -# 5) Install this service -# sudo service hass-daemon install -# 6) Restart Machine -# -# After installation, HA should start automatically. If HA does not start, -# check the log file output for errors. -# /var/opt/homeassistant/home-assistant.log - -PRE_EXEC="" -RUN_AS="USER" -PID_FILE="/var/run/hass.pid" -CONFIG_DIR="/var/opt/homeassistant" -FLAGS="-v --config $CONFIG_DIR --pid-file $PID_FILE --daemon" -REDIRECT="> $CONFIG_DIR/home-assistant.log 2>&1" - -start() { - if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE) 2> /dev/null; then - echo 'Service already running' >&2 - return 1 - fi - echo 'Starting service…' >&2 - local CMD="$PRE_EXEC hass $FLAGS $REDIRECT;" - su -c "$CMD" $RUN_AS - echo 'Service started' >&2 -} - -stop() { - if [ ! -f "$PID_FILE" ] || ! kill -0 $(cat "$PID_FILE") 2> /dev/null; then - echo 'Service not running' >&2 - return 1 - fi - echo 'Stopping service…' >&2 - kill $(cat "$PID_FILE") - while ps -p $(cat "$PID_FILE") > /dev/null 2>&1; do sleep 1;done; - echo 'Service stopped' >&2 -} - -install() { - echo "Installing Home Assistant Daemon (hass-daemon)" - echo "999999" > $PID_FILE - chown $RUN_AS $PID_FILE - mkdir -p $CONFIG_DIR - chown $RUN_AS $CONFIG_DIR -} - -uninstall() { - echo -n "Are you really sure you want to uninstall this service? That cannot be undone. [yes|No] " - local SURE - read SURE - if [ "$SURE" = "yes" ]; then - stop - rm -fv "$PID_FILE" - echo "Notice: The config directory has not been removed" - echo $CONFIG_DIR - update-rc.d -f hass-daemon remove - rm -fv "$0" - echo "Home Assistant Daemon has been removed. Home Assistant is still installed." - fi -} - -case "$1" in - start) - start - ;; - stop) - stop - ;; - install) - install - ;; - uninstall) - uninstall - ;; - restart) - stop - start - ;; - *) - echo "Usage: $0 {start|stop|restart|install|uninstall}" -esac diff --git a/script/home-assistant@.service b/script/home-assistant@.service deleted file mode 100644 index 8e520952db9..00000000000 --- a/script/home-assistant@.service +++ /dev/null @@ -1,20 +0,0 @@ -# This is a simple service file for systems with systemd to tun HA as user. -# -# For details please check https://home-assistant.io/getting-started/autostart/ -# -[Unit] -Description=Home Assistant for %i -After=network.target - -[Service] -Type=simple -User=%i -# Enable the following line if you get network-related HA errors during boot -#ExecStartPre=/usr/bin/sleep 60 -# Use `whereis hass` to determine the path of hass -ExecStart=/usr/bin/hass --runner -SendSIGKILL=no -RestartForceExitStatus=100 - -[Install] -WantedBy=multi-user.target diff --git a/script/nginx-hass b/script/nginx-hass deleted file mode 100644 index 274fa105e04..00000000000 --- a/script/nginx-hass +++ /dev/null @@ -1,120 +0,0 @@ -## -# -# Home Assistant - nginx Configuration File -# -# Using nginx as a proxy for Home Assistant allows you to serve Home Assisatnt -# securely over standard ports. This configuration file and instructions will -# walk you through setting up Home Assistant over a secure connection. -# -# 1) Get a domain name forwarded to your IP. -# Chances are, you have a dynamic IP Address (your ISP changes your address -# periodically). If this is true, you can use a Dynamic DNS service to obtain -# a domain and set it up to update with you IP. If you purchase your own -# domain name, you will be able to easily get a trusted SSL certificate -# later. -# -# -# 2) Install nginx on your server. -# This will vary depending on your OS. Check out Google for this. After -# installing, ensure that nginx is not running. -# -# -# 3) Obtain an SSL certificate. -# -# 3a) Using Let's Encrypt -# If you purchased your own domain, you can use https://letsencrypt.org/ to -# obtain a free, publicly trusted SSL certificate. This will allow you to -# work with services like IFTTT. Download and install per the instructions -# online and get a certificate using the following command. -# -# ./letsencrypt-auto certonly --standalone -d example.com -d www.example.com -# -# Instead of example.com, use your domain. You will need to renew this -# certificate every 90 days. -# -# 3b) Using openssl -# If you do not own your own domain, you may generate a self-signed -# certificate. This will not work with IFTTT, but it will encrypt all of your -# Home Assistant traffic. -# -# openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 9999 -# sudo cp key.pem cert.pem /etc/nginx/ssl -# sudo chmod 600 /etc/nginx/ssl/key.pem /etc/nginx/ssl/cert.pem -# sudo chown root:root /etc/nginx/ssl/key.pem /etc/nginx/ssl/cert.pem -# -# -# 4) Create dhparams file -# As a fair warning, this file will take a while to generate. -# -# cd /etc/nginx/ssl -# sudo openssl dhparam -out dhparams.pem 2048 -# -# -# 5) Install this configuration file in nginx. -# -# cp nginx-hass /etc/nginx/sites-available/hass -# cd /etc/nginx/sites-enabled -# sudo unlink default -# sudo ln ../sites-available/hass default -# -# -# 6) Double check this configuration to ensure all settings are correct and -# start nginx. -# -# -# 7) Forward ports 443 and 80 to your server on your router. Do not forward -# port 8123. -# -## -http { - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } - - server { - # Update this line to be your domain - server_name example.com; - - # These shouldn't need to be changed - listen 80 default_server; - listen [::]:80 default_server ipv6only=on; - return 301 https://$host$request_uri; - } - - server { - # Update this line to be your domain - server_name example.com; - - # Ensure these lines point to your SSL certificate and key - ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; - # Use these lines instead if you created a self-signed certificate - # ssl_certificate /etc/nginx/ssl/cert.pem; - # ssl_certificate_key /etc/nginx/ssl/key.pem; - - # Ensure this line points to your dhparams file - ssl_dhparam /etc/nginx/ssl/dhparams.pem; - - - # These shouldn't need to be changed - listen 443 default_server; - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; - ssl on; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - - proxy_buffering off; - - location / { - proxy_pass http://localhost:8123; - proxy_set_header Host $host; - proxy_redirect http:// https://; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - } -} From d3b62e1fe1a97c0a6178ab488d63856f11558bc5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Dec 2016 10:18:00 -0800 Subject: [PATCH 133/137] Requirements use zip instead of git (#4692) --- homeassistant/components/nest.py | 4 ++-- homeassistant/components/nuimo_controller.py | 3 ++- requirements_all.txt | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index a606cb488d4..01f7d6ab287 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -18,8 +18,8 @@ _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) REQUIREMENTS = [ - 'git+https://github.com/technicalpickles/python-nest.git' - '@0be5c8a6307ee81540f21aac4fcd22cc5d98c988' # nest-cam branch + 'http://github.com/technicalpickles/python-nest' + '/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip' # nest-cam branch '#python-nest==3.0.0'] DOMAIN = 'nest' diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py index 756ae1cf223..e9fd41bd098 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller.py @@ -13,7 +13,8 @@ from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = [ '--only-binary=all ' # avoid compilation of gattlib - 'git+https://github.com/getSenic/nuimo-linux-python' + 'http://github.com/getSenic/nuimo-linux-python' + '/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip' '#nuimo==1.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 35fad590e86..371e51129f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ aiohttp==1.1.6 async_timeout==1.1.0 # homeassistant.components.nuimo_controller ---only-binary=all git+https://github.com/getSenic/nuimo-linux-python#nuimo==1.0.0 +--only-binary=all http://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 # homeassistant.components.isy994 PyISY==1.0.7 @@ -131,9 +131,6 @@ fuzzywuzzy==0.14.0 # homeassistant.components.device_tracker.bluetooth_le_tracker # gattlib==0.20150805 -# homeassistant.components.nest -git+https://github.com/technicalpickles/python-nest.git@0be5c8a6307ee81540f21aac4fcd22cc5d98c988#python-nest==3.0.0 - # homeassistant.components.notify.gntp gntp==1.0.3 @@ -167,6 +164,9 @@ hikvision==0.4 # homeassistant.components.sensor.dht # http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0 +# homeassistant.components.nest +http://github.com/technicalpickles/python-nest/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip#python-nest==3.0.0 + # homeassistant.components.light.flux_led https://github.com/Danielhiversen/flux_led/archive/0.9.zip#flux_led==0.9 From 9a6c9cff306c3d7652edab1723fd1ec1d98b2ba1 Mon Sep 17 00:00:00 2001 From: GadgetReactor Date: Sun, 4 Dec 2016 03:38:14 +0800 Subject: [PATCH 134/137] Update reference to correct tplink switch (#4670) --- homeassistant/components/switch/tplink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index bcc1b329fa8..41c1d0462b3 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/GadgetReactor/pyHS100/archive/' - '1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0'] + 'fadb76c5a0e04f4995f16055845ffedc6d658316.zip#pyHS100==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 371e51129f6..22d0d563c6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ http://github.com/technicalpickles/python-nest/archive/0be5c8a6307ee81540f21aac4 https://github.com/Danielhiversen/flux_led/archive/0.9.zip#flux_led==0.9 # homeassistant.components.switch.tplink -https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0 +https://github.com/GadgetReactor/pyHS100/archive/fadb76c5a0e04f4995f16055845ffedc6d658316.zip#pyHS100==0.2.1 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7 From dddf4d14605dc85c6a6d0eb9ef85a6989cd1b567 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 3 Dec 2016 20:46:04 +0100 Subject: [PATCH 135/137] Style 0.34 (#4689) * Minor style updates * Minor style updates * Update validation and logger messages * Update ordering * Fix lint issue * Fix line too long * Update ordering * update logger messages --- homeassistant/components/camera/amcrest.py | 14 +-- homeassistant/components/camera/nest.py | 25 +++-- .../components/media_player/dunehd.py | 20 ++-- homeassistant/components/remote/__init__.py | 20 ++-- homeassistant/components/remote/harmony.py | 92 +++++++++---------- homeassistant/components/wink.py | 22 ++--- 6 files changed, 92 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index ff862f2db11..97ed45f21e4 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -5,13 +5,14 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.amcrest/ """ import logging + import voluptuous as vol +import homeassistant.loader as loader from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) from homeassistant.helpers import config_validation as cv -import homeassistant.loader as loader REQUIREMENTS = ['amcrest==1.0.0'] @@ -33,19 +34,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup an Amcrest IP Camera.""" + """Set up an Amcrest IP Camera.""" from amcrest import AmcrestCamera - data = AmcrestCamera(config.get(CONF_HOST), - config.get(CONF_PORT), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + data = AmcrestCamera( + config.get(CONF_HOST), config.get(CONF_PORT), + config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) persistent_notification = loader.get_component('persistent_notification') try: data.camera.current_time # pylint: disable=broad-except except Exception as ex: - _LOGGER.error('Unable to connect to Amcrest camera: %s', str(ex)) + _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) persistent_notification.create( hass, 'Error: {}
    ' 'You will need to restart hass after fixing.' diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py index 20003d4b347..8bda0e8eb9c 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -4,25 +4,26 @@ Support for Nest Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.nest/ """ - import logging from datetime import timedelta + import requests -from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) + import homeassistant.components.nest as nest +from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.util.dt import utcnow +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['nest'] -_LOGGER = logging.getLogger(__name__) + +NEST_BRAND = 'Nest' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) -NEST_BRAND = "Nest" - def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup a Nest Cam.""" + """Set up a Nest Cam.""" if discovery_info is None: return camera_devices = hass.data[nest.DATA_NEST].camera_devices() @@ -39,14 +40,12 @@ class NestCamera(Camera): super(NestCamera, self).__init__() self.structure = structure self.device = device - - # data attributes self._location = None self._name = None self._is_online = None self._is_streaming = None self._is_video_history_enabled = False - # default to non-NestAware subscribed, but will be fixed during update + # Default to non-NestAware subscribed, but will be fixed during update self._time_between_snapshots = timedelta(seconds=30) self._last_image = None self._next_snapshot_at = None @@ -68,10 +67,10 @@ class NestCamera(Camera): @property def brand(self): - """Camera Brand.""" + """Return the brand of the camera.""" return NEST_BRAND - # this doesn't seem to be getting called regularly, for some reason + # This doesn't seem to be getting called regularly, for some reason def update(self): """Cache value from Python-nest.""" self._location = self.device.where @@ -84,7 +83,7 @@ class NestCamera(Camera): # NestAware allowed 10/min self._time_between_snapshots = timedelta(seconds=6) else: - # otherwise, 2/min + # Otherwise, 2/min self._time_between_snapshots = timedelta(seconds=30) def _ready_for_snapshot(self, now): @@ -100,7 +99,7 @@ class NestCamera(Camera): try: response = requests.get(url) except requests.exceptions.RequestException as error: - _LOGGER.error('Error getting camera image: %s', error) + _LOGGER.error("Error getting camera image: %s", error) return None self._next_snapshot_at = now + self._time_between_snapshots diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py index 7c28ff1190d..d0851a2e088 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -2,23 +2,23 @@ DuneHD implementation of the media player. For more details about this platform, please refer to the documentation -https://home-assistant.io/components/dunehd/ +https://home-assistant.io/components/media_player.dunehd/ """ +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, MediaPlayerDevice) + SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, + MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_ON, STATE_PLAYING) -import homeassistant.helpers.config_validation as cv -import voluptuous as vol - REQUIREMENTS = ['pdunehd==1.3'] -DEFAULT_NAME = "DuneHD" +DEFAULT_NAME = 'DuneHD' -CONF_SOURCES = "sources" +CONF_SOURCES = 'sources' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -33,7 +33,7 @@ DUNEHD_PLAYER_SUPPORT = \ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the media player demo platform.""" + """Set up the media player demo platform.""" sources = config.get(CONF_SOURCES, {}) from pdunehd import DuneHDPlayer diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 57d816fd0c9..8223c33d944 100755 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -9,6 +9,7 @@ import logging import os import voluptuous as vol + from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -18,22 +19,25 @@ from homeassistant.const import ( from homeassistant.components import group from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -ATTR_DEVICE = 'device' -ATTR_COMMAND = 'command' +_LOGGER = logging.getLogger(__name__) + ATTR_ACTIVITY = 'activity' -SERVICE_SEND_COMMAND = 'send_command' -SERVICE_SYNC = 'sync' +ATTR_COMMAND = 'command' +ATTR_DEVICE = 'device' DOMAIN = 'remote' -SCAN_INTERVAL = 30 -GROUP_NAME_ALL_REMOTES = 'all remotes' ENTITY_ID_ALL_REMOTES = group.ENTITY_ID_FORMAT.format('all_remotes') - ENTITY_ID_FORMAT = DOMAIN + '.{}' +GROUP_NAME_ALL_REMOTES = 'all remotes' + MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +SCAN_INTERVAL = 30 +SERVICE_SEND_COMMAND = 'send_command' +SERVICE_SYNC = 'sync' + REMOTE_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -47,8 +51,6 @@ REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({ vol.Required(ATTR_COMMAND): cv.string, }) -_LOGGER = logging.getLogger(__name__) - def is_on(hass, entity_id=None): """Return if the remote is on based on the statemachine.""" diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 9e799fab066..1bd1e1b94cc 100755 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -3,35 +3,38 @@ Support for Harmony Hub devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.harmony/ - """ - import logging from os import path import urllib.parse + +import voluptuous as vol + +import homeassistant.components.remote as remote +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID) from homeassistant.components.remote import PLATFORM_SCHEMA, DOMAIN from homeassistant.util import slugify from homeassistant.config import load_yaml_config_file -import homeassistant.components.remote as remote -import homeassistant.helpers.config_validation as cv -import voluptuous as vol - REQUIREMENTS = ['pyharmony==1.0.12'] + _LOGGER = logging.getLogger(__name__) ATTR_DEVICE = 'device' ATTR_COMMAND = 'command' ATTR_ACTIVITY = 'activity' +DEFAULT_PORT = 5222 +DEVICES = [] + SERVICE_SYNC = 'harmony_sync' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(ATTR_ACTIVITY, default=None): cv.string, }) @@ -39,37 +42,32 @@ HARMONY_SYNC_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) -# List of devices that have been registered -DEVICES = [] - def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Harmony platform.""" + """Set up the Harmony platform.""" import pyharmony global DEVICES name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - _LOGGER.info('Loading Harmony platform: ' + name) + _LOGGER.debug("Loading Harmony platform: %s", name) - harmony_conf_file = hass.config.path('harmony_' + slugify(name) + '.conf') + harmony_conf_file = hass.config.path( + '{}{}{}'.format('harmony_', slugify(name), '.conf')) try: - _LOGGER.debug('calling pyharmony.ha_get_token for remote at: ' + - host + ':' + port) + _LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s", + host, port) token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port)) except ValueError as err: - _LOGGER.critical(err.args[0] + ' for remote: ' + name) + _LOGGER.warning("%s for remote: %s", err.args[0], name) return False - _LOGGER.debug('received token: ' + token) - DEVICES = [HarmonyRemote(config.get(CONF_NAME), - config.get(CONF_HOST), - config.get(CONF_PORT), - config.get(ATTR_ACTIVITY), - harmony_conf_file, - token)] + _LOGGER.debug("Received token: %s", token) + DEVICES = [HarmonyRemote( + config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT), + config.get(ATTR_ACTIVITY), harmony_conf_file, token)] add_devices(DEVICES, True) register_services(hass) return True @@ -80,10 +78,9 @@ def register_services(hass): descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_SYNC, - _sync_service, - descriptions.get(SERVICE_SYNC), - schema=HARMONY_SYNC_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_SYNC, _sync_service, descriptions.get(SERVICE_SYNC), + schema=HARMONY_SYNC_SCHEMA) def _apply_service(service, service_func, *service_func_args): @@ -113,7 +110,7 @@ class HarmonyRemote(remote.RemoteDevice): import pyharmony from pathlib import Path - _LOGGER.debug('HarmonyRemote device init started for: ' + name) + _LOGGER.debug("HarmonyRemote device init started for: %s", name) self._name = name self._ip = host self._port = port @@ -122,10 +119,11 @@ class HarmonyRemote(remote.RemoteDevice): self._default_activity = activity self._token = token self._config_path = out_path - _LOGGER.debug('retrieving harmony config using token: ' + token) + _LOGGER.debug("Retrieving harmony config using token: %s", token) self._config = pyharmony.ha_get_config(self._token, host, port) if not Path(self._config_path).is_file(): - _LOGGER.debug('writing harmony configuration to file: ' + out_path) + _LOGGER.debug("Writing harmony configuration to file: %s", + out_path) pyharmony.ha_write_config_file(self._config, self._config_path) @property @@ -147,12 +145,10 @@ class HarmonyRemote(remote.RemoteDevice): """Return current activity.""" import pyharmony name = self._name - _LOGGER.debug('polling ' + name + ' for current activity') - state = pyharmony.ha_get_current_activity(self._token, - self._config, - self._ip, - self._port) - _LOGGER.debug(name + '\'s current activity reported as: ' + state) + _LOGGER.debug("Polling %s for current activity", name) + state = pyharmony.ha_get_current_activity( + self._token, self._config, self._ip, self._port) + _LOGGER.debug("%s current activity reported as: %s", name, state) self._current_activity = state self._state = bool(state != 'PowerOff') @@ -165,14 +161,11 @@ class HarmonyRemote(remote.RemoteDevice): activity = self._default_activity if activity: - pyharmony.ha_start_activity(self._token, - self._ip, - self._port, - self._config, - activity) + pyharmony.ha_start_activity( + self._token, self._ip, self._port, self._config, activity) self._state = True else: - _LOGGER.error('No activity specified with turn_on service') + _LOGGER.error("No activity specified with turn_on service") def turn_off(self): """Start the PowerOff activity.""" @@ -182,17 +175,16 @@ class HarmonyRemote(remote.RemoteDevice): def send_command(self, **kwargs): """Send a command to one device.""" import pyharmony - pyharmony.ha_send_command(self._token, self._ip, - self._port, kwargs[ATTR_DEVICE], - kwargs[ATTR_COMMAND]) + pyharmony.ha_send_command( + self._token, self._ip, self._port, kwargs[ATTR_DEVICE], + kwargs[ATTR_COMMAND]) def sync(self): """Sync the Harmony device with the web service.""" import pyharmony - _LOGGER.debug('syncing hub with Harmony servers') + _LOGGER.debug("Syncing hub with Harmony servers") pyharmony.ha_sync(self._token, self._ip, self._port) - self._config = pyharmony.ha_get_config(self._token, - self._ip, - self._port) - _LOGGER.debug('writing hub config to file: ' + self._config_path) + self._config = pyharmony.ha_get_config( + self._token, self._ip, self._port) + _LOGGER.debug("Writing hub config to file: %s", self._config_path) pyharmony.ha_write_config_file(self._config, self._config_path) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index dbd7d8760ce..bb374afaf86 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -9,10 +9,9 @@ import logging import voluptuous as vol from homeassistant.helpers import discovery -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL, \ - CONF_EMAIL, CONF_PASSWORD, \ - EVENT_HOMEASSISTANT_START, \ - EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -56,7 +55,7 @@ WINK_COMPONENTS = [ def setup(hass, config): - """Setup the Wink component.""" + """Set up the Wink component.""" import pywink from pubnubsubhandler import PubNubSubscriptionHandler @@ -95,7 +94,7 @@ def setup(hass, config): def force_update(call): """Force all devices to poll the Wink API.""" - _LOGGER.info("Refreshing Wink states from API.") + _LOGGER.info("Refreshing Wink states from API") for entity in hass.data[DOMAIN]['entities']: entity.update_ha_state(True) hass.services.register(DOMAIN, 'Refresh state from Wink', force_update) @@ -115,22 +114,21 @@ class WinkDevice(Entity): self.wink = wink self._battery = self.wink.battery_level hass.data[DOMAIN]['pubnub'].add_subscription( - self.wink.pubnub_channel, - self._pubnub_update) + self.wink.pubnub_channel, self._pubnub_update) hass.data[DOMAIN]['entities'].append(self) def _pubnub_update(self, message): try: if message is None: - _LOGGER.error("Error on pubnub update for " + self.name + - " pollin API for current state") + _LOGGER.error("Error on pubnub update for %s " + "polling API for current state", self.name) self.update_ha_state(True) else: self.wink.pubnub_update(message) self.update_ha_state() except (ValueError, KeyError, AttributeError): - _LOGGER.error("Error in pubnub JSON for " + self.name + - " pollin API for current state") + _LOGGER.error("Error in pubnub JSON for %s " + "polling API for current state", self.name) self.update_ha_state(True) @property From 4904653b700d029e0642732f5247daa7bcbaeb90 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Dec 2016 11:59:05 -0800 Subject: [PATCH 136/137] Yarl has been fixed (#4694) --- requirements_all.txt | 1 - setup.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 22d0d563c6e..56772281632 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,6 @@ pip>=7.0.0 jinja2>=2.8 voluptuous==0.9.2 typing>=3,<4 -yarl==0.7.1 aiohttp==1.1.6 async_timeout==1.1.0 diff --git a/setup.py b/setup.py index 9bd1719d38b..d6f4792c186 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ DOWNLOAD_URL = ('{}/archive/' PACKAGES = find_packages(exclude=['tests', 'tests.*']) -# Pinning yarl because yarl 0.8 breaks aiohttp REQUIRES = [ 'requests>=2,<3', 'pyyaml>=3.11,<4', @@ -22,7 +21,6 @@ REQUIRES = [ 'jinja2>=2.8', 'voluptuous==0.9.2', 'typing>=3,<4', - 'yarl==0.7.1', 'aiohttp==1.1.6', 'async_timeout==1.1.0', ] From 69d3a3dd3268f7e480e6ff71ce666da2362214b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Dec 2016 12:16:18 -0800 Subject: [PATCH 137/137] Version bump to 0.34 --- .../components/frontend/www_static/home-assistant-polymer | 2 +- homeassistant/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 4d7e1fef752..b76ad67d4ab 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 4d7e1fef752196345bcd18c69dc03334c60a6a18 +Subproject commit b76ad67d4abbc0cc492fc11842c9d163b4917ead diff --git a/homeassistant/const.py b/homeassistant/const.py index 64a4e7e5c45..8e45ec4bb43 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 34 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)
  • VKGk!Ps=B1NhuBdie&?BP zOi8^_i#4p)EniV$>m;hOEK#e`QDl`)8fWjL2vtuh)k)pGQhb8qBId?xFX{9a&zc~x zoiUHA@av9QVzauP4r%n8UeM`Oo_WGRP4c?a^9=13EzfoCW-U@#Y_z=aoCc@h!H|WE zTC@Z+k9LOjXBZtipy4{(Nx%bancSMHeSAxVGNSt&dw{;56d zGG~2s(K;c=c~P(3Q9)v=;zU7*Q>y)`(~icx zFi2H+Z1H@_^#n$(ClS|pdnRdwX4HGI1{_P3;p*A1V&WCZb=Bm@wWytvzQTG*(~71Q z^K^BHPMzr5&B5{5cBeGEo00DnHy4E`O-BRS1cU@PO6Yb8EXn0~Wa1gTD9N>h%`q}ZZV zIpdhU@Yh{4&Yq1DJ1yMnmnHPHLDf51woPDS%&~|SDvRtVe`O2V7`3J~Q^7FckdseA zPlu0zwi8p&#u&y^|7LFBH;A6PDAHSa#$TP&A(NgvDgRn2X6fm2PBh_0=bfoxH}=_H zdvwUIXT$412lgM_H0$ft*{w4QIRvFtHmv+0VC(Y4*@?ru?-b_;`9F3?-c5euWpIV- zn(l!`ClybrUTeHkEg;Hm*euq$ATVB>qxVNwY9;I0>owc9xg9ZiDPl^eVG|wW(t9$Di*DcxA4V;ypiny%wwevU20Lr>Wk4Nm-d6KDS># z5k9%@jYE~wHqnDYGb&pzHH*y@xFh(n&G~!Y&y*~ic$Q`VZUoMIefQTU#+>TAGrs8W zxOMp%_x@L=r<{A6vn-}qPbx^-v?Q+5+-xFhm22tO4j*&=*AasH8HII*dP+}a=i6Ve zUe+h;R2sPGS-aUR2KT;auij0)xu>dWE_akn!1+VP>!nlsU;PRbzv>qHzh~Lq)AM&` zJeOswomq2trSS3Ni97#>k{MIrl{)BfmGx zJv&}v-+DLezS@~b>M70tBDW^*(LbMWn}6x#;a#tveY$S8QtQYS&4Rw8g;ze7>oz|K z5_wjf+I8uzO$PIt_owcfbvBp?XLtUE*5fly%%1V`uf~_mE2bB+x>6gLTsfaIS-oP@YMp~V{28aW-gzi5e9e1u z@RGHD9Ws1#xzgKa`c9~opD5IJxOv_Cd6xTkzuT;y=l{jLV$zS&Ym)m9@NV&ledV3_tlrMQ zdfHC?dNx^pxw>{s#@jh_W>f~p^TnC@_DX)q@N}qk{enWDU%y#+t=7;R9XA8 z?0M7?S1%o(%*@{c&kpaN`7ZD1!JPI-O#D@^wI!-wZ`!wXqy4v6Pv31jTD|68d49$Q z{~NFSox}U@D_Gt&zb3Gu@BQ8HCpP6Bw2gQdSeq_g@};z3tl4cf~mi zvJ3P!`d*oS&iQ-F-Iyog!e>O+>?~0Fw@vbz+M_2&4zF1^;WKN|zR-@d453!LrmYsM zKEL2rN{whq_B!)}`etc2qJCdmc`wJh=JCIs1qyY)A6A$M?AYCXc&+(Q`Jk)vJyS&( F7yz07YPSFY literal 2325 zcmb2|=HRI7vx;D1E>0~f%S=v-%cvw5uV^9QzLWzOHJV>HIWL?2T{4ho>L; zS1;T1HP2sv?SrZhay)$xwyf0+&E<;q$g8*6-*n36ajNEyKd+kd_q^Yg@!;*Mx%uw< zKj+=u$@xn>zVPtJ>+9wm==<4r{$2m$3Hf=`3i!7Fo2YtldiLhzTaE9|DY2?b9Qd0T z_TyoRRju;A1{2$=zv}uL3un#zS+HZC)@v@?jgyw;ZR2h`Y+JaGP5Jpc<7cOn?#wI~ zK67@F&+($%=EwE%4KuI5x>Nc4*e&Le*fYz-#n0XND0|{l$V3zVR29h-&i*3VG_8IO&WmbKYrYjO%Cp*hrQ)Qv z#_f$ws|6$G_oplgnjm?W<%y3fr?1G#(s?TkPAPdVaAy3PV&uCuMP<3>$;C6e4W>jM zQM~4KL8Hs>wWd~Ve*kOeWP`~Lid>;`mrXh!MJ&w}TRQ1uuN0f$V@|(iQK^+;-72bz ze#~=39=*sk)inukZ3#Sdc#6R(7rBMK6BCq<%r_CV;nF<&sdw3wNgBuYK0C13gX`SX zNuFwteFVfVh1f4RS-`xQC-;(xx={E<5ntt<7c%`!IwQrFopj?*RdFpgnzz)F^=trZ zp0B#4_tK_6j1PEprY#M174;R;Td~Y2MJZS^+*2g2@>~(Cr$pYt$C3{7)SEn74|GnL zsBxZk*5v0Zwntfgw3U@i7R~L_2s8<966%{FIBj7kXMg3Eiw825oRyRtdOXEW+w@Di z?+ctzs3Y;xG@^maa#|_#ViT{z)ha#_eiE0omIu8EDDquAr(x2;>DQw4jE`OR2=EfH zlRehrd}`(*EhUGHUFzkV8wI60W<74|@j1oP(hnC(ozsWeQy*mYpc&!Na*wQ7FbO zYT1dd2@Fd^G#qZ)3xDHFTRq7$W z_rL-{0nW}sg|BihN1M(sIO=p^#c5Wj!}^9g`3rW6Y~E^W3ifHgm1lUuDPRvf^>a>WHL$rzS>dIkvTLbW12Y%JS*u z=KEJ0uGcUIFW)Pd+jZ!LmQ?Q?W@J})CuTAGoxb^zh zlKO*Z=b7#QRJlG-E=%Ua!+>`lhx$&6mA?D?P4r(b^iS~&ehgv7Z8leRYX zzmK!IZE?)>-b~}tzS@;G=ciXc6MGwd=k#*RU7K8{INm+H{KC52kc7L_D)w&dKXlTr z;MnaXqnq2xa`!eY*1cPmA6x$5c3$GoMa9c*eSRkYS;XtL;LNYuFd z?&I?<^PkJI)y{}{cck-i^5#2rE6kr{S)0Ty-1hg|s{;(&b7w{||K*ZrIsE8cah-Td z>Rt1y6a6pkT4VR+3vc=%cymGh!SbH(PoGDhpI!3F;N7l$f0n9-w$89({kHK&pVhy0 zsRA*d57n&k3EXv;?-s-3-9_6wAALFa;Dbd`{;O-w&D(wVYD94a?ESqZ^M`LkYQ@8Q z&c-_)J>~i!QGC8RRGjriTFXJV3)klEaKC=&2j9QVn|N0IdCxKTP^|c#obz+V-pVKL zw$avNZ+!Vrc1}szx8@C7+tis3WI}s1Q@JyS_>TWdY?W+4E-(~caPvMopcP)&vT<-2PwPLye^`>%11b)BI3wt8()-+iZ*ey4R8WCU;-Y zT)X&Pu+M_*f^~+zSFWGiSiNbT{->WTJDUu}{Z{SIowe1owCtJUYRhYXxU% Date: Mon, 28 Nov 2016 02:45:49 +0100 Subject: [PATCH 079/137] Support for media_position property on media_player (#4172) * Added support for media_position property to media_player + implementation for sonos. * Pla yback progress now updates without needed state transitions in HA. * Linting fixes * media_position_update_at property is now a datetime. * Minor fix. * Linting fixes. --- .../components/media_player/__init__.py | 17 +++++ .../components/media_player/sonos.py | 70 ++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c9df431965b..d0cacd47b75 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -59,6 +59,8 @@ ATTR_MEDIA_SEEK_POSITION = 'seek_position' ATTR_MEDIA_CONTENT_ID = 'media_content_id' ATTR_MEDIA_CONTENT_TYPE = 'media_content_type' ATTR_MEDIA_DURATION = 'media_duration' +ATTR_MEDIA_POSITION = 'media_position' +ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at' ATTR_MEDIA_TITLE = 'media_title' ATTR_MEDIA_ARTIST = 'media_artist' ATTR_MEDIA_ALBUM_NAME = 'media_album_name' @@ -120,6 +122,8 @@ ATTR_TO_PROPERTY = [ ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME, @@ -447,6 +451,19 @@ class MediaPlayerDevice(Entity): """Duration of current playing media in seconds.""" return None + @property + def media_position(self): + """Position of current playing media in seconds.""" + return None + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + return None + @property def media_image_url(self): """Image url of current playing media.""" diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 5b070e1f5bd..022e5742ee7 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID) from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import utcnow REQUIREMENTS = ['SoCo==0.12'] @@ -264,6 +265,8 @@ class SonosDevice(MediaPlayerDevice): self._coordinator = None self._media_content_id = None self._media_duration = None + self._media_position = None + self._media_position_updated_at = None self._media_image_url = None self._media_artist = None self._media_album_name = None @@ -404,6 +407,9 @@ class SonosDevice(MediaPlayerDevice): media_album_name = track_info.get('album') media_title = track_info.get('title') + media_position = None + media_position_updated_at = None + is_radio_stream = \ current_media_uri.startswith('x-sonosapi-stream:') or \ current_media_uri.startswith('x-rincon-mp3radio:') @@ -425,7 +431,6 @@ class SonosDevice(MediaPlayerDevice): media_image_url = None elif is_radio_stream: - is_radio_stream = True media_image_url = self._format_media_image_url( current_media_uri ) @@ -489,6 +494,46 @@ class SonosDevice(MediaPlayerDevice): support_next_track = True support_pause = True + position_info = self._player.avTransport.GetPositionInfo( + [('InstanceID', 0), + ('Channel', 'Master')] + ) + rel_time = _parse_timespan( + position_info.get("RelTime") + ) + + # player no longer reports position? + update_media_position = rel_time is None and \ + self._media_position is not None + + # player started reporting position? + update_media_position |= rel_time is not None and \ + self._media_position is None + + # position changed? + if rel_time is not None and \ + self._media_position is not None: + + time_diff = utcnow() - self._media_position_updated_at + time_diff = time_diff.total_seconds() + + calculated_position = \ + self._media_position + \ + time_diff + + update_media_position = \ + abs(calculated_position - rel_time) > 1.5 + + if update_media_position: + media_position = rel_time + media_position_updated_at = utcnow() + else: + # don't update media_position (don't want unneeded + # state transitions) + media_position = self._media_position + media_position_updated_at = \ + self._media_position_updated_at + playlist_position = track_info.get('playlist_position') if playlist_position in ('', 'NOT_IMPLEMENTED', None): playlist_position = None @@ -514,6 +559,8 @@ class SonosDevice(MediaPlayerDevice): self._media_duration = _parse_timespan( track_info.get('duration') ) + self._media_position = media_position + self._media_position_updated_at = media_position_updated_at self._media_image_url = media_image_url self._media_artist = media_artist self._media_album_name = media_album_name @@ -541,6 +588,8 @@ class SonosDevice(MediaPlayerDevice): self._coordinator = None self._media_content_id = None self._media_duration = None + self._media_position = None + self._media_position_updated_at = None self._media_image_url = None self._media_artist = None self._media_album_name = None @@ -642,6 +691,25 @@ class SonosDevice(MediaPlayerDevice): else: return self._media_duration + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self._coordinator: + return self._coordinator.media_position + else: + return self._media_position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid. + + Returns value from homeassistant.util.dt.utcnow(). + """ + if self._coordinator: + return self._coordinator.media_position_updated_at + else: + return self._media_position_updated_at + @property def media_image_url(self): """Image url of current playing media.""" From 44a508e86c73affb230c910826aecc7deb382490 Mon Sep 17 00:00:00 2001 From: Harris Borawski Date: Sun, 27 Nov 2016 21:11:49 -0800 Subject: [PATCH 080/137] Add exception handling to Sonarr (#4569) * Add exception handling to request call to prevent failure in setup_platform if host is down * update for comments * update test for state being none * remove unused import --- homeassistant/components/sensor/sonarr.py | 23 +++++++++++++++++---- tests/components/sensor/test_sonarr.py | 25 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py index 9f4ed3d0581..62f33556c17 100644 --- a/homeassistant/components/sensor/sonarr.py +++ b/homeassistant/components/sensor/sonarr.py @@ -86,6 +86,7 @@ class SonarrSensor(Entity): self.ssl = 's' if conf.get(CONF_SSL) else '' # Object data + self.data = [] self._tz = timezone(str(hass.config.time_zone)) self.type = sensor_type self._name = SENSOR_TYPES[self.type][0] @@ -96,16 +97,24 @@ class SonarrSensor(Entity): self._icon = SENSOR_TYPES[self.type][2] # Update sensor + self._available = False self.update() def update(self): """Update the data for the sensor.""" start = get_date(self._tz) end = get_date(self._tz, self.days) - res = requests.get( - ENDPOINTS[self.type].format( - self.ssl, self.host, self.port, self.apikey, start, end), - timeout=5) + try: + res = requests.get( + ENDPOINTS[self.type].format( + self.ssl, self.host, self.port, self.apikey, start, end), + timeout=5) + except OSError: + _LOGGER.error('Host %s is not available', self.host) + self._available = False + self._state = None + return + if res.status_code == 200: if self.type in ['upcoming', 'queue', 'series', 'commands']: if self.days == 1 and self.type == 'upcoming': @@ -146,6 +155,7 @@ class SonarrSensor(Entity): self._unit ) ) + self._available = True @property def name(self): @@ -157,6 +167,11 @@ class SonarrSensor(Entity): """Return sensor state.""" return self._state + @property + def available(self): + """Return sensor availability.""" + return self._available + @property def unit_of_measurement(self): """Return the unit of the sensor.""" diff --git a/tests/components/sensor/test_sonarr.py b/tests/components/sensor/test_sonarr.py index c44d01f3ce0..cc9186677dc 100644 --- a/tests/components/sensor/test_sonarr.py +++ b/tests/components/sensor/test_sonarr.py @@ -10,6 +10,11 @@ from homeassistant.components.sensor import sonarr from tests.common import get_test_home_assistant +def mocked_exception(*args, **kwargs): + """Mock exception thrown by requests.get.""" + raise OSError + + def mocked_requests_get(*args, **kwargs): """Mock requests.get invocations.""" class MockResponse: @@ -814,3 +819,23 @@ class TestSonarrSetup(unittest.TestCase): 'S04E11', device.device_state_attributes["Bob's Burgers"] ) + + @unittest.mock.patch('requests.get', side_effect=mocked_exception) + def test_exception_handling(self, req_mock): + """Tests exception being handled""" + config = { + 'platform': 'sonarr', + 'api_key': 'foo', + 'days': '1', + 'unit': 'GB', + "include_paths": [ + '/data' + ], + 'monitored_conditions': [ + 'upcoming' + ] + } + sonarr.setup_platform(self.hass, config, self.add_devices, None) + for device in self.DEVICES: + device.update() + self.assertEqual(None, device.state) From 1e6c660f599b115615b033869e1525529663d884 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 Nov 2016 06:55:26 +0100 Subject: [PATCH 081/137] Threshold sensor (#4216) * Add threshold sensor * New config requirement, update async, other changes, and update tests * Update threshold.py --- .../components/binary_sensor/threshold.py | 128 ++++++++++++++++++ .../binary_sensor/test_threshold.py | 98 ++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 homeassistant/components/binary_sensor/threshold.py create mode 100644 tests/components/binary_sensor/test_threshold.py diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py new file mode 100644 index 00000000000..4dc11a3c5c7 --- /dev/null +++ b/homeassistant/components/binary_sensor/threshold.py @@ -0,0 +1,128 @@ +""" +Support for monitoring if a sensor value is below/above a threshold. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.threshold/ +""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA) +from homeassistant.const import ( + CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS, + ATTR_ENTITY_ID) +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change + +_LOGGER = logging.getLogger(__name__) + +ATTR_SENSOR_VALUE = 'sensor_value' +ATTR_THRESHOLD = 'threshold' +ATTR_TYPE = 'type' + +CONF_LOWER = 'lower' +CONF_THRESHOLD = 'threshold' +CONF_UPPER = 'upper' + +DEFAULT_NAME = 'Threshold' + +SENSOR_TYPES = [CONF_LOWER, CONF_UPPER] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_THRESHOLD): vol.Coerce(float), + vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Threshold sensor.""" + entity_id = config.get(CONF_ENTITY_ID) + name = config.get(CONF_NAME) + threshold = config.get(CONF_THRESHOLD) + limit_type = config.get(CONF_TYPE) + sensor_class = config.get(CONF_SENSOR_CLASS) + + yield from async_add_devices( + [ThresholdSensor(hass, entity_id, name, threshold, limit_type, + sensor_class)], True) + return True + + +class ThresholdSensor(BinarySensorDevice): + """Representation of a Threshold sensor.""" + + def __init__(self, hass, entity_id, name, threshold, limit_type, + sensor_class): + """Initialize the Threshold sensor.""" + self._hass = hass + self._entity_id = entity_id + self.is_upper = limit_type == 'upper' + self._name = name + self._threshold = threshold + self._sensor_class = sensor_class + self._deviation = False + self.sensor_value = 0 + + @callback + # pylint: disable=invalid-name + def async_threshold_sensor_state_listener( + entity, old_state, new_state): + """Called when the sensor changes state.""" + if new_state.state == STATE_UNKNOWN: + return + + try: + self.sensor_value = float(new_state.state) + except ValueError: + _LOGGER.error("State is not numerical") + + hass.async_add_job(self.async_update_ha_state, True) + + async_track_state_change( + hass, entity_id, async_threshold_sensor_state_listener) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._deviation + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def sensor_class(self): + """Return the sensor class of the sensor.""" + return self._sensor_class + + @property + def state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ENTITY_ID: self._entity_id, + ATTR_SENSOR_VALUE: self.sensor_value, + ATTR_THRESHOLD: self._threshold, + ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER, + } + + @asyncio.coroutine + def async_update(self): + """Get the latest data and updates the states.""" + if self.is_upper: + self._deviation = bool(self.sensor_value > self._threshold) + else: + self._deviation = bool(self.sensor_value < self._threshold) diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py new file mode 100644 index 00000000000..6af2bbe5b39 --- /dev/null +++ b/tests/components/binary_sensor/test_threshold.py @@ -0,0 +1,98 @@ +"""The test for the threshold sensor platform.""" +import unittest + +from homeassistant.bootstrap import setup_component +from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + +from tests.common import get_test_home_assistant + + +class TestThresholdSensor(unittest.TestCase): + """Test the threshold sensor.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_sensor_upper(self): + """Test if source is above threshold.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'threshold': '15', + 'type': 'upper', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + self.assertEqual('upper', state.attributes.get('type')) + self.assertEqual('sensor.test_monitored', + state.attributes.get('entity_id')) + self.assertEqual(16, state.attributes.get('sensor_value')) + self.assertEqual(float(config['binary_sensor']['threshold']), + state.attributes.get('threshold')) + + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 14) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 15) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.threshold') + + assert state.state == 'off' + + def test_sensor_lower(self): + """Test if source is below threshold.""" + config = { + 'binary_sensor': { + 'platform': 'threshold', + 'threshold': '15', + 'name': 'Test_threshold', + 'type': 'lower', + 'entity_id': 'sensor.test_monitored', + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 16) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_threshold') + + self.assertEqual('lower', state.attributes.get('type')) + + assert state.state == 'off' + + self.hass.states.set('sensor.test_monitored', 14) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_threshold') + + assert state.state == 'on' + + self.hass.states.set('sensor.test_monitored', 15) + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_threshold') + + assert state.state == 'off' From d8c4af9c81d5d6ff0b5d8a749c9a441d00c5a803 Mon Sep 17 00:00:00 2001 From: Mark King Date: Mon, 28 Nov 2016 06:01:13 +0000 Subject: [PATCH 082/137] TEMPer component: reset devices on address change (#4596) Fixes https://github.com/home-assistant/home-assistant/issues/4389 The USB address of these devices periodically changes, causing home-assistant to fail to read the temperature data. This PR fixes this by re-reading the available devices on failure. I've been running this for several days and for the first time have consistent temperature data without having to restart home-assistant. --- homeassistant/components/sensor/temper.py | 50 ++++++++++++++++------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index b7fcdd1b015..4ccd1be7d76 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -24,27 +24,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float) }) +TEMPER_SENSORS = [] + + +def get_temper_devices(): + """Scan the Temper devices from temperusb.""" + from temperusb.temper import TemperHandler + return TemperHandler().get_devices() + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Temper sensors.""" - from temperusb.temper import TemperHandler - temp_unit = hass.config.units.temperature_unit name = config.get(CONF_NAME) scaling = { 'scale': config.get(CONF_SCALE), 'offset': config.get(CONF_OFFSET) } - temper_devices = TemperHandler().get_devices() - devices = [] + temper_devices = get_temper_devices() for idx, dev in enumerate(temper_devices): if idx != 0: name = name + '_' + str(idx) - devices.append(TemperSensor(dev, temp_unit, name, scaling)) + TEMPER_SENSORS.append(TemperSensor(dev, temp_unit, name, scaling)) + add_devices(TEMPER_SENSORS) - add_devices(devices) + +def reset_devices(): + """ + Re-scan for underlying Temper sensors and assign them to our devices. + + This assumes the same sensor devices are present in the same order. + """ + temper_devices = get_temper_devices() + for sensor, device in zip(TEMPER_SENSORS, temper_devices): + sensor.set_temper_device(device) class TemperSensor(Entity): @@ -52,18 +67,12 @@ class TemperSensor(Entity): def __init__(self, temper_device, temp_unit, name, scaling): """Initialize the sensor.""" - self.temper_device = temper_device self.temp_unit = temp_unit self.scale = scaling['scale'] self.offset = scaling['offset'] self.current_value = None self._name = name - - # set calibration data - self.temper_device.set_calibration_data( - scale=self.scale, - offset=self.offset - ) + self.set_temper_device(temper_device) @property def name(self): @@ -80,6 +89,16 @@ class TemperSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self.temp_unit + def set_temper_device(self, temper_device): + """Assign the underlying device for this sensor.""" + self.temper_device = temper_device + + # set calibration data + self.temper_device.set_calibration_data( + scale=self.scale, + offset=self.offset + ) + def update(self): """Retrieve latest state.""" try: @@ -88,5 +107,6 @@ class TemperSensor(Entity): sensor_value = self.temper_device.get_temperature(format_str) self.current_value = round(sensor_value, 1) except IOError: - _LOGGER.error('Failed to get temperature due to insufficient ' - 'permissions. Try running with "sudo"') + _LOGGER.error('Failed to get temperature. The device address may' + 'have changed - attempting to reset device') + reset_devices() From 248a90b71d40530eadc97ee5f64c080576078dbd Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Nov 2016 07:13:22 +0100 Subject: [PATCH 083/137] Added denon media player controls via denonavr library (#4580) * Added denonavr module again * Edited requirements_all.txt * Edited .coveragerc * Fixed error with AUX1 input source in library * Adding device should not fail on connection timeout * Changed method to select source * Update requirements_all.txt --- .coveragerc | 1 + .../components/media_player/denonavr.py | 245 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 249 insertions(+) create mode 100644 homeassistant/components/media_player/denonavr.py diff --git a/.coveragerc b/.coveragerc index b34811a6173..66fd541fcaf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -186,6 +186,7 @@ omit = homeassistant/components/media_player/cast.py homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py + homeassistant/components/media_player/denonavr.py homeassistant/components/media_player/directv.py homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py new file mode 100644 index 00000000000..6db223bc6ec --- /dev/null +++ b/homeassistant/components/media_player/denonavr.py @@ -0,0 +1,245 @@ +""" +Support for Denon AVR receivers using their HTTP interface. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.denon/ +""" + +import logging +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, + MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON, + MEDIA_TYPE_MUSIC) +from homeassistant.const import ( + CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED, + CONF_NAME, STATE_ON) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['denonavr==0.1.6'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = None + +SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \ + SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Denon platform.""" + import denonavr + + receiver = denonavr.DenonAVR(config.get(CONF_HOST), config.get(CONF_NAME)) + + add_devices([DenonDevice(receiver)]) + _LOGGER.info("Denon receiver at host %s initialized", + config.get(CONF_HOST)) + + +class DenonDevice(MediaPlayerDevice): + """Representation of a Denon Media Player Device.""" + + def __init__(self, receiver): + """Initialize the device.""" + self._receiver = receiver + self._name = self._receiver.name + self._muted = self._receiver.muted + self._volume = self._receiver.volume + self._current_source = self._receiver.input_func + self._source_list = self._receiver.input_func_list + self._state = self._receiver.state + self._power = self._receiver.power + self._media_image_url = self._receiver.image_url + self._title = self._receiver.title + self._artist = self._receiver.artist + self._album = self._receiver.album + self._band = self._receiver.band + self._frequency = self._receiver.frequency + self._station = self._receiver.station + + def update(self): + """Get the latest status information from device.""" + # Update denonavr + self._receiver.update() + # Refresh own data + self._name = self._receiver.name + self._muted = self._receiver.muted + self._volume = self._receiver.volume + self._current_source = self._receiver.input_func + self._source_list = self._receiver.input_func_list + self._state = self._receiver.state + self._power = self._receiver.power + self._media_image_url = self._receiver.image_url + self._title = self._receiver.title + self._artist = self._receiver.artist + self._album = self._receiver.album + self._band = self._receiver.band + self._frequency = self._receiver.frequency + self._station = self._receiver.station + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + # Volume is send in a format like -50.0. Minimum is around -80.0 + return (float(self._volume) + 80) / 100 + + @property + def source(self): + """Return the current input source.""" + return self._current_source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_DENON + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return None + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self._state == STATE_PLAYING or self._state == STATE_PAUSED: + return MEDIA_TYPE_MUSIC + else: + return MEDIA_TYPE_CHANNEL + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return None + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self._power == "ON": + return self._media_image_url + else: + return None + + @property + def media_title(self): + """Title of current playing media.""" + if self._title is not None: + return self._title + else: + return self._frequency + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + if self._artist is not None: + return self._artist + else: + return self._band + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + if self._album is not None: + return self._album + else: + return self._station + + @property + def media_album_artist(self): + """Album artist of current playing media, music track only.""" + return None + + @property + def media_track(self): + """Track number of current playing media, music track only.""" + return None + + @property + def media_series_title(self): + """Title of series of current playing media, TV show only.""" + return None + + @property + def media_season(self): + """Season of current playing media, TV show only.""" + return None + + @property + def media_episode(self): + """Episode of current playing media, TV show only.""" + return None + + def media_play_pause(self): + """Simulate play pause media player.""" + return self._receiver.toggle_play_pause() + + def media_previous_track(self): + """Send previous track command.""" + return self._receiver.previous_track() + + def media_next_track(self): + """Send next track command.""" + return self._receiver.next_track() + + def select_source(self, source): + """Select input source.""" + return self._receiver.set_input_func(source) + + def turn_on(self): + """Turn on media player.""" + if self._receiver.power_on(): + self._state = STATE_ON + return True + else: + return False + + def turn_off(self): + """Turn off media player.""" + if self._receiver.power_off(): + self._state = STATE_OFF + return True + else: + return False + + def volume_up(self): + """Volume up the media player.""" + return self._receiver.volume_up() + + def volume_down(self): + """Volume down media player.""" + return self._receiver.volume_down() + + def mute_volume(self, mute): + """Send mute command.""" + return self._receiver.mute(mute) diff --git a/requirements_all.txt b/requirements_all.txt index dfb56d186a1..8406b293780 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -72,6 +72,9 @@ colorlog>2.1,<3 # homeassistant.components.binary_sensor.concord232 concord232==0.14 +# homeassistant.components.media_player.denonavr +denonavr==0.1.6 + # homeassistant.components.media_player.directv directpy==0.1 From 9db1ff8cd4950b7aa3c5c6b2942602c5f448194f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Nov 2016 22:27:02 -0800 Subject: [PATCH 084/137] Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 2 +- .../frontend/www_static/frontend.html.gz | Bin 130286 -> 130592 bytes .../www_static/home-assistant-polymer | 2 +- .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 2322 -> 2322 bytes 6 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index fabb6a58da8..d5686fff834 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -2,7 +2,7 @@ FINGERPRINTS = { "core.js": "526d7d704ae478c30ae20c1426c2e4f4", - "frontend.html": "c65df08be08a7329ee01a273af02d6a4", + "frontend.html": "5baa4dc3b109ca80d4c282fb12c6c23a", "mdi.html": "46a76f877ac9848899b8ed382427c16f", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 292b75ec27e..83d01bb470f 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2,4 +2,4 @@ },_distributeDirtyRoots:function(){for(var e,t=this.shadyRoot._dirtyRoots,o=0,i=t.length;o0?~setTimeout(e,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(e<0)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;t \ No newline at end of file +this.currentTarget=t,this.defaultPrevented=!1,this.eventPhase=Event.AT_TARGET,this.timeStamp=Date.now()},i=window.Element.prototype.animate;window.Element.prototype.animate=function(n,r){var o=i.call(this,n,r);o._cancelHandlers=[],o.oncancel=null;var a=o.cancel;o.cancel=function(){a.call(this);var i=new e(this,null,t()),n=this._cancelHandlers.concat(this.oncancel?[this.oncancel]:[]);setTimeout(function(){n.forEach(function(t){t.call(i.target,i)})},0)};var s=o.addEventListener;o.addEventListener=function(t,e){"function"==typeof e&&"cancel"==t?this._cancelHandlers.push(e):s.call(this,t,e)};var u=o.removeEventListener;return o.removeEventListener=function(t,e){if("cancel"==t){var i=this._cancelHandlers.indexOf(e);i>=0&&this._cancelHandlers.splice(i,1)}else u.call(this,t,e)},o}}}(),function(t){var e=document.documentElement,i=null,n=!1;try{var r=getComputedStyle(e).getPropertyValue("opacity"),o="0"==r?"1":"0";i=e.animate({opacity:[o,o]},{duration:1}),i.currentTime=0,n=getComputedStyle(e).getPropertyValue("opacity")==o}catch(t){}finally{i&&i.cancel()}if(!n){var a=window.Element.prototype.animate;window.Element.prototype.animate=function(e,i){return window.Symbol&&Symbol.iterator&&Array.prototype.from&&e[Symbol.iterator]&&(e=Array.from(e)),Array.isArray(e)||null===e||(e=t.convertToArrayForm(e)),a.call(this,e,i)}}}(c),!function(t,e,i){function n(t){var i=e.timeline;i.currentTime=t,i._discardAnimations(),0==i._animations.length?o=!1:requestAnimationFrame(n)}var r=window.requestAnimationFrame;window.requestAnimationFrame=function(t){return r(function(i){e.timeline._updateAnimationsPromises(),t(i),e.timeline._updateAnimationsPromises()})},e.AnimationTimeline=function(){this._animations=[],this.currentTime=void 0},e.AnimationTimeline.prototype={getAnimations:function(){return this._discardAnimations(),this._animations.slice()},_updateAnimationsPromises:function(){e.animationsWithPromises=e.animationsWithPromises.filter(function(t){return t._updatePromises()})},_discardAnimations:function(){this._updateAnimationsPromises(),this._animations=this._animations.filter(function(t){return"finished"!=t.playState&&"idle"!=t.playState})},_play:function(t){var i=new e.Animation(t,this);return this._animations.push(i),e.restartWebAnimationsNextTick(),i._updatePromises(),i._animation.play(),i._updatePromises(),i},play:function(t){return t&&t.remove(),this._play(t)}};var o=!1;e.restartWebAnimationsNextTick=function(){o||(o=!0,requestAnimationFrame(n))};var a=new e.AnimationTimeline;e.timeline=a;try{Object.defineProperty(window.document,"timeline",{configurable:!0,get:function(){return a}})}catch(t){}try{window.document.timeline=a}catch(t){}}(c,e,f),function(t,e,i){e.animationsWithPromises=[],e.Animation=function(e,i){if(this.id="",e&&e._id&&(this.id=e._id),this.effect=e,e&&(e._animation=this),!i)throw new Error("Animation with null timeline is not supported");this._timeline=i,this._sequenceNumber=t.sequenceNumber++,this._holdTime=0,this._paused=!1,this._isGroup=!1,this._animation=null,this._childAnimations=[],this._callback=null,this._oldPlayState="idle",this._rebuildUnderlyingAnimation(),this._animation.cancel(),this._updatePromises()},e.Animation.prototype={_updatePromises:function(){var t=this._oldPlayState,e=this.playState;return this._readyPromise&&e!==t&&("idle"==e?(this._rejectReadyPromise(),this._readyPromise=void 0):"pending"==t?this._resolveReadyPromise():"pending"==e&&(this._readyPromise=void 0)),this._finishedPromise&&e!==t&&("idle"==e?(this._rejectFinishedPromise(),this._finishedPromise=void 0):"finished"==e?this._resolveFinishedPromise():"finished"==t&&(this._finishedPromise=void 0)),this._oldPlayState=this.playState,this._readyPromise||this._finishedPromise},_rebuildUnderlyingAnimation:function(){this._updatePromises();var t,i,n,r,o=!!this._animation;o&&(t=this.playbackRate,i=this._paused,n=this.startTime,r=this.currentTime,this._animation.cancel(),this._animation._wrapper=null,this._animation=null),(!this.effect||this.effect instanceof window.KeyframeEffect)&&(this._animation=e.newUnderlyingAnimationForKeyframeEffect(this.effect),e.bindAnimationForKeyframeEffect(this)),(this.effect instanceof window.SequenceEffect||this.effect instanceof window.GroupEffect)&&(this._animation=e.newUnderlyingAnimationForGroup(this.effect),e.bindAnimationForGroup(this)),this.effect&&this.effect._onsample&&e.bindAnimationForCustomEffect(this),o&&(1!=t&&(this.playbackRate=t),null!==n?this.startTime=n:null!==r?this.currentTime=r:null!==this._holdTime&&(this.currentTime=this._holdTime),i&&this.pause()),this._updatePromises()},_updateChildren:function(){if(this.effect&&"idle"!=this.playState){var t=this.effect._timing.delay;this._childAnimations.forEach(function(i){this._arrangeChildren(i,t),this.effect instanceof window.SequenceEffect&&(t+=e.groupChildDuration(i.effect))}.bind(this))}},_setExternalAnimation:function(t){if(this.effect&&this._isGroup)for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index f67f4d68a9ed1c23329e7b0d387d74aaa8edff09..5a1e6faa46691b278f6c57c63febf3dba4607044 100644 GIT binary patch delta 47704 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

    DB3HWfGz7 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 Jriw5y002WWc`E<_ literal 2322 zcmb2|=HQTQvyNb5E>0~f%S=v9Z_+x*pWn=8;-Whk}kE`B~(S0bA_m|y{b6@N%*~@$->c=Bu>d2McznpSzy$>*8Pc|8L@~^2XkgbpFf{nTKemJ|NDDvX2q){qb{<) zbuwvAWxkZgmv!N2PF2~n9@+bWD$MIr)Lg}$Stc(O@|0`s5j?o`+>YXRkKOK;q^