From bcfc30264df4b7678d8ce416fdb6395a5f5e601c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Jan 2017 08:40:20 -0800 Subject: [PATCH 001/191] version bump to 0.37 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b5c04c26d0b..3ef79ad1724 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 = 36 -PATCH_VERSION = '0' +MINOR_VERSION = 37 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 82b84f480b9b3f30d634298121407345ea29010e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Jan 2017 09:16:46 -0800 Subject: [PATCH 002/191] Fix script release (#5345) --- script/release | 3 --- 1 file changed, 3 deletions(-) diff --git a/script/release b/script/release index 3bcddcfef76..65a6339cedc 100755 --- a/script/release +++ b/script/release @@ -1,9 +1,6 @@ #!/bin/sh # Pushes a new version to PyPi. -# Stop on errors -set -e - cd "$(dirname "$0")/.." head -n 5 homeassistant/const.py | tail -n 1 | grep PATCH_VERSION > /dev/null From 633c1408fbc293e8846e7da02e03970d0fbf2279 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 15 Jan 2017 18:37:17 +0100 Subject: [PATCH 003/191] Upgrade pylast to 1.7.0 (#5344) --- homeassistant/components/sensor/lastfm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 5d660f20217..5da512b205d 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pylast==1.6.0'] +REQUIREMENTS = ['pylast==1.7.0'] CONF_USERS = 'users' diff --git a/requirements_all.txt b/requirements_all.txt index ca9f9f422a7..3bea17fbead 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -431,7 +431,7 @@ pyicloud==0.9.1 pyiss==1.0.1 # homeassistant.components.sensor.lastfm -pylast==1.6.0 +pylast==1.7.0 # homeassistant.components.litejet pylitejet==0.1 From 8200827a19c43a9d38428b3ec6eae7946208d8b5 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sun, 15 Jan 2017 14:37:24 -0500 Subject: [PATCH 004/191] Add support for direct MJPEG streams from Amcrest cameras (#5217) * Add support for direct MJPEG streams from Amcrest cameras The previous implementation relied on using snapshots from the camera. However, some Amcrest models cannot keep up with the large number of requests and instead timeout, resulting in no video stream. These cameras do provide MJPEG streams though, so this commit adds the option to use these instead of creating one from snapshots. Unfortunately, some cameras on newer firmwares do not support MJPEG streams at high resolution - only at low resolution. By providing users with both a `resolution` and `stream_source` option, we can allow them to choose whichever combination works for their particular model and firmware version. * Close the stream instead of releasing it * Close stream without creating a task * Handle client aborts * fix lint --- homeassistant/components/camera/amcrest.py | 84 +++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index c6568677583..a3d8dcf35df 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -4,8 +4,13 @@ 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 asyncio import logging +import aiohttp +from aiohttp import web +from aiohttp.web_exceptions import HTTPGatewayTimeout +import async_timeout import voluptuous as vol import homeassistant.loader as loader @@ -13,16 +18,19 @@ 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 +from homeassistant.helpers.aiohttp_client import async_create_clientsession REQUIREMENTS = ['amcrest==1.0.0'] _LOGGER = logging.getLogger(__name__) CONF_RESOLUTION = 'resolution' +CONF_STREAM_SOURCE = 'stream_source' DEFAULT_NAME = 'Amcrest Camera' DEFAULT_PORT = 80 DEFAULT_RESOLUTION = 'high' +DEFAULT_STREAM_SOURCE = 'mjpeg' NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_TITLE = 'Amcrest Camera Setup' @@ -32,6 +40,14 @@ RESOLUTION_LIST = { 'low': 1, } +STREAM_SOURCE_LIST = { + 'mjpeg': 0, + 'snapshot': 1 +} + +CONTENT_TYPE_HEADER = 'Content-Type' +TIMEOUT = 5 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, @@ -40,6 +56,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.In(RESOLUTION_LIST)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): + vol.All(vol.In(STREAM_SOURCE_LIST)), }) @@ -64,19 +82,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None): notification_id=NOTIFICATION_ID) return False - add_devices([AmcrestCam(config, data)]) + add_devices([AmcrestCam(hass, config, data)]) return True class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" - def __init__(self, device_info, data): + def __init__(self, hass, device_info, data): """Initialize an Amcrest camera.""" super(AmcrestCam, self).__init__() + self._base_url = '%s://%s:%s/cgi-bin' % ( + 'http', + device_info.get(CONF_HOST), + device_info.get(CONF_PORT) + ) self._data = data + self._hass = hass self._name = device_info.get(CONF_NAME) self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] + self._stream_source = STREAM_SOURCE_LIST[ + device_info.get(CONF_STREAM_SOURCE) + ] + self._token = self._auth = aiohttp.BasicAuth( + device_info.get(CONF_USERNAME), + password=device_info.get(CONF_PASSWORD) + ) + self._websession = async_create_clientsession(hass) def camera_image(self): """Return a still image reponse from the camera.""" @@ -84,6 +116,54 @@ class AmcrestCam(Camera): response = self._data.camera.snapshot(channel=self._resolution) return response.data + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Return an MJPEG stream.""" + # The snapshot implementation is handled by the parent class + if self._stream_source == STREAM_SOURCE_LIST['snapshot']: + yield from super().handle_async_mjpeg_stream(request) + return + + # Otherwise, stream an MJPEG image stream directly from the camera + streaming_url = '%s/mjpg/video.cgi?channel=0&subtype=%d' % ( + self._base_url, + self._resolution + ) + + stream = None + response = None + try: + with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + stream = yield from self._websession.get( + streaming_url, + auth=self._token, + timeout=TIMEOUT + ) + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + + yield from response.prepare(request) + + while True: + data = yield from stream.content.read(16384) + if not data: + break + response.write(data) + + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", streaming_url) + raise HTTPGatewayTimeout() + + except asyncio.CancelledError: + _LOGGER.debug("Close stream by frontend.") + response = None + + finally: + if stream is not None: + stream.close() + if response is not None: + yield from response.write_eof() + @property def name(self): """Return the name of this camera.""" From 01d9e6cdfe611ce3eb6477679c15d955aaeda5e9 Mon Sep 17 00:00:00 2001 From: freol35241 Date: Sun, 15 Jan 2017 20:53:46 +0100 Subject: [PATCH 005/191] Removing throttle decorator Removing redundant throttle decorator on update method. This ensures the existing 'cache-value' config option is respected. Also, UPDATE_INTERVAL is renamed to DEFAULT_UPDATE_INTERVAL for clarity. --- homeassistant/components/sensor/miflora.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index a519d97a855..155808f702f 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC) @@ -31,9 +30,7 @@ DEFAULT_MEDIAN = 3 DEFAULT_NAME = 'Mi Flora' DEFAULT_RETRIES = 2 DEFAULT_TIMEOUT = 10 - -UPDATE_INTERVAL = 1200 -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=UPDATE_INTERVAL) +DEFAULT_UPDATE_INTERVAL = 1200 # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -53,7 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, - vol.Optional(CONF_CACHE, default=UPDATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, }) @@ -122,7 +119,6 @@ class MiFloraSensor(Entity): """Force update.""" return self._force_update - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """ Update current conditions. From c8cf952e2190b70eb1fed2ee89dace23d1f91bd4 Mon Sep 17 00:00:00 2001 From: freol35241 Date: Sun, 15 Jan 2017 21:10:02 +0100 Subject: [PATCH 006/191] Remove import of datetime module --- homeassistant/components/sensor/miflora.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 155808f702f..1922d4832ee 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -4,7 +4,6 @@ Support for Xiaomi Mi Flora BLE plant sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.miflora/ """ -from datetime import timedelta import logging import voluptuous as vol From ad23613cdc541dbc64bc88bc7433ceb8d4e2b55b Mon Sep 17 00:00:00 2001 From: Heiko Rothe Date: Sun, 15 Jan 2017 22:45:54 +0100 Subject: [PATCH 007/191] Fixed the lannouncer platform get_service method (#5352) --- homeassistant/components/notify/lannouncer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/lannouncer.py b/homeassistant/components/notify/lannouncer.py index be1bc636fd6..4d038faeb9a 100644 --- a/homeassistant/components/notify/lannouncer.py +++ b/homeassistant/components/notify/lannouncer.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ _LOGGER = logging.getLogger(__name__) -def get_service(hass, config): +def get_service(hass, config, discovery_info=None): """Get the Lannouncer notification service.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) From 6b91d9a75cdb5ebe7d0ef6a33fa6cda7146585f8 Mon Sep 17 00:00:00 2001 From: Gianluca Barbaro Date: Sun, 15 Jan 2017 23:05:41 +0100 Subject: [PATCH 008/191] Update keyboard_remote.py (#5341) Now it fires events in case the keyboard disconnects and/or disconnects --- homeassistant/components/keyboard_remote.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index de7eacf96dd..992f9390124 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -48,6 +48,8 @@ REQUIREMENTS = ['evdev==0.6.1'] _LOGGER = logging.getLogger(__name__) ICON = 'mdi:remote' KEYBOARD_REMOTE_COMMAND_RECEIVED = 'keyboard_remote_command_received' +KEYBOARD_REMOTE_CONNECTED = 'keyboard_remote_connected' +KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected' KEY_CODE = 'key_code' KEY_VALUE = {'key_up': 0, 'key_down': 1, 'key_hold': 2} TYPE = 'type' @@ -151,13 +153,19 @@ class KeyboardRemote(threading.Thread): self.keyboard_connected = True _LOGGER.debug('KeyboardRemote: keyboard re-connected, %s', self.device_descriptor) + self.hass.bus.fire( + KEYBOARD_REMOTE_CONNECTED + ) try: event = self.dev.read_one() except IOError: # Keyboard Disconnected self.keyboard_connected = False - _LOGGER.debug('KeyboardRemote: keyard disconnected, %s', + _LOGGER.debug('KeyboardRemote: keyboard disconnected, %s', self.device_descriptor) + self.hass.bus.fire( + KEYBOARD_REMOTE_DISCONNECTED + ) continue if not event: From 2e3d5302bfe1806a61e1860c089bda69d4318a8d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 15 Jan 2017 23:53:37 +0100 Subject: [PATCH 009/191] Bugfix timedelta v2 (#5349) * Bugfix timedelta v2 * fix volvo * fix lint --- .../device_tracker/bluetooth_le_tracker.py | 14 +++++--------- .../components/device_tracker/bluetooth_tracker.py | 4 +--- .../components/device_tracker/volvooncall.py | 5 ++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index 8c29bc94be5..454ab127af0 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -1,14 +1,12 @@ """Tracking for bluetooth low energy devices.""" import logging -from datetime import timedelta import voluptuous as vol from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - PLATFORM_SCHEMA, load_config, DEFAULT_TRACK_NEW + PLATFORM_SCHEMA, load_config ) -import homeassistant.util as util import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv @@ -86,14 +84,13 @@ def setup_scanner(hass, config, see): # if track new devices is true discover new devices # on every scan. - track_new = util.convert(config.get(CONF_TRACK_NEW), bool, - DEFAULT_TRACK_NEW) + track_new = config.get(CONF_TRACK_NEW) + if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False - interval = util.convert(config.get(CONF_SCAN_INTERVAL), int, - DEFAULT_SCAN_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) def update_ble(now): """Lookup Bluetooth LE devices and update status.""" @@ -113,8 +110,7 @@ def setup_scanner(hass, config, see): _LOGGER.info("Discovered Bluetooth LE device %s", address) see_device(address, devs[address], new_device=True) - track_point_in_utc_time(hass, update_ble, - now + timedelta(seconds=interval)) + track_point_in_utc_time(hass, update_ble, now + interval) update_ble(dt_util.utcnow()) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 86e115c65c4..a8b3861cdc5 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -1,6 +1,5 @@ """Tracking for bluetooth devices.""" import logging -from datetime import timedelta import voluptuous as vol @@ -83,8 +82,7 @@ def setup_scanner(hass, config, see): see_device((mac, result)) except bluetooth.BluetoothError: _LOGGER.exception('Error looking up bluetooth device!') - track_point_in_utc_time(hass, update_bluetooth, - now + timedelta(seconds=interval)) + track_point_in_utc_time(hass, update_bluetooth, now + interval) update_bluetooth(dt_util.utcnow()) diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index a7dba230831..1be76d6139c 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -37,7 +37,7 @@ def setup_scanner(hass, config, see): config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) - interval = max(MIN_TIME_BETWEEN_SCANS.seconds, + interval = max(MIN_TIME_BETWEEN_SCANS, config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) def _see_vehicle(vehicle): @@ -91,8 +91,7 @@ def setup_scanner(hass, config, see): return True finally: - track_point_in_utc_time(hass, update, - now + timedelta(seconds=interval)) + track_point_in_utc_time(hass, update, now + interval) _LOGGER.info('Logging in to service') return update(utcnow()) From 7a1d4b96ef4a6a2317dcbc5cf6901e2781fd27c2 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 16 Jan 2017 06:48:36 +0100 Subject: [PATCH 010/191] Eq3bt bump version, expose away attribute (#5353) * eq3bt: read away ends attr * eq3bt: bump version to fix missing __init__, expose away_ends attribute. --- homeassistant/components/climate/eq3btsmart.py | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 41697f7b31d..a8ab9bd30b2 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -17,7 +17,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.2'] +REQUIREMENTS = ['python-eq3bt==0.1.4'] _LOGGER = logging.getLogger(__name__) @@ -29,6 +29,7 @@ ATTR_STATE_WINDOW_OPEN = "window_open" ATTR_STATE_VALVE = "valve" ATTR_STATE_LOCKED = "is_locked" ATTR_STATE_LOW_BAT = "low_battery" +ATTR_STATE_AWAY_END = "away_end" DEVICE_SCHEMA = vol.Schema({ vol.Required(CONF_MAC): cv.string, @@ -152,6 +153,7 @@ class EQ3BTSmartThermostat(ClimateDevice): ATTR_STATE_LOW_BAT: self._thermostat.low_battery, ATTR_STATE_VALVE: self._thermostat.valve_state, ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open, + ATTR_STATE_AWAY_END: self._thermostat.away_end, } return dev_specific diff --git a/requirements_all.txt b/requirements_all.txt index 3bea17fbead..6d3f254f02a 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -476,7 +476,7 @@ pysnmp==4.3.2 python-digitalocean==1.10.1 # homeassistant.components.climate.eq3btsmart -python-eq3bt==0.1.2 +python-eq3bt==0.1.4 # homeassistant.components.sensor.darksky python-forecastio==1.3.5 From f08e2648aed98f7ba4f3876a9ddd42dd729c0cb3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 16 Jan 2017 11:06:23 +0100 Subject: [PATCH 011/191] Bugfix upc with aiohttp 1.2 (cookies) (#5362) --- .../components/device_tracker/upc_connect.py | 17 +++++------ tests/test_util/aiohttp.py | 29 ++++++++++++------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index aafa9824a4e..13336e939a5 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -74,8 +74,11 @@ class UPCDeviceScanner(DeviceScanner): return [] raw = yield from self._async_ws_function(CMD_DEVICES) - xml_root = ET.fromstring(raw) + if raw is None: + _LOGGER.warning("Can't read device from %s", self.host) + return + xml_root = ET.fromstring(raw) return [mac.text for mac in xml_root.iter('MACAddr')] @asyncio.coroutine @@ -94,7 +97,8 @@ class UPCDeviceScanner(DeviceScanner): "http://{}/common_page/login.html".format(self.host) ) - self.token = self._async_get_token() + yield from response.text() + self.token = response.cookies['sessionToken'].value # login data = yield from self._async_ws_function(CMD_LOGIN, { @@ -144,7 +148,7 @@ class UPCDeviceScanner(DeviceScanner): # load data, store token for next request raw = yield from response.text() - self.token = self._async_get_token() + self.token = response.cookies['sessionToken'].value return raw @@ -155,10 +159,3 @@ class UPCDeviceScanner(DeviceScanner): finally: if response is not None: yield from response.release() - - def _async_get_token(self): - """Extract token from cookies.""" - cookie_manager = self.websession.cookie_jar.filter_cookies( - "http://{}".format(self.host)) - - return cookie_manager.get('sessionToken') diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index dcdf69395b4..afe2f626de7 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -37,11 +37,9 @@ class AiohttpClientMocker: content = b'' if params: url = str(yarl.URL(url).with_query(params)) - if cookies: - self._cookies.update(cookies) self._mocks.append(AiohttpClientMockResponse( - method, url, status, content, exc)) + method, url, status, content, cookies, exc)) def get(self, *args, **kwargs): """Register a mock get request.""" @@ -68,10 +66,6 @@ class AiohttpClientMocker: """Number of requests made.""" return len(self.mock_calls) - def filter_cookies(self, host): - """Return hosts cookies.""" - return self._cookies - def clear_requests(self): """Reset mock calls.""" self._mocks.clear() @@ -97,7 +91,7 @@ class AiohttpClientMocker: class AiohttpClientMockResponse: """Mock Aiohttp client response.""" - def __init__(self, method, url, status, response, exc=None): + def __init__(self, method, url, status, response, cookies=None, exc=None): """Initialize a fake response.""" self.method = method self._url = url @@ -107,6 +101,14 @@ class AiohttpClientMockResponse: self.response = response self.exc = exc + self._cookies = {} + + if cookies: + for name, data in cookies.items(): + cookie = mock.MagicMock() + cookie.value = data + self._cookies[name] = cookie + def match_request(self, method, url, params=None): """Test if response answers request.""" if method.lower() != self.method.lower(): @@ -140,6 +142,11 @@ class AiohttpClientMockResponse: return True + @property + def cookies(self): + """Return dict of cookies.""" + return self._cookies + @asyncio.coroutine def read(self): """Return mock response.""" @@ -160,6 +167,10 @@ class AiohttpClientMockResponse: """Mock release.""" pass + def close(self): + """Mock close.""" + pass + @contextmanager def mock_aiohttp_client(): @@ -173,6 +184,4 @@ def mock_aiohttp_client(): setattr(instance, method, functools.partial(mocker.match_request, method)) - instance.cookie_jar.filter_cookies = mocker.filter_cookies - yield mocker From 887c586aae436607ec3464bbbea4e7e800b763bb Mon Sep 17 00:00:00 2001 From: whhsw Date: Mon, 16 Jan 2017 19:44:09 +0800 Subject: [PATCH 012/191] Add station parameter to waqi sensor (#5239) * Add station parameter to waqi sensor * Update waqi.py * Update waqi.py * Update waqi.py * add back 'waqi' prefix and add station_name prop * Update waqi.py --- homeassistant/components/sensor/waqi.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index b893eeaf204..af2d80a0948 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -30,6 +30,7 @@ ATTR_TIME = 'time' ATTRIBUTION = 'Data provided by the World Air Quality Index project' CONF_LOCATIONS = 'locations' +CONF_STATIONS = 'stations' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -38,7 +39,8 @@ SENSOR_TYPES = { } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_LOCATIONS): cv.ensure_list + vol.Optional(CONF_STATIONS): cv.ensure_list, + vol.Required(CONF_LOCATIONS): cv.ensure_list, }) @@ -47,11 +49,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pwaqi dev = [] + station_filter = config.get(CONF_STATIONS) for location_name in config.get(CONF_LOCATIONS): station_ids = pwaqi.findStationCodesByCity(location_name) - _LOGGER.error('The following stations were returned: %s', station_ids) + _LOGGER.info('The following stations were returned: %s', station_ids) for station in station_ids: - dev.append(WaqiSensor(WaqiData(station), station)) + waqi_sensor = WaqiSensor(WaqiData(station), station) + if (not station_filter) or \ + (waqi_sensor.station_name in station_filter): + dev.append(WaqiSensor(WaqiData(station), station)) add_devices(dev) @@ -74,6 +80,14 @@ class WaqiSensor(Entity): except (KeyError, TypeError): return 'WAQI {}'.format(self._station_id) + @property + def station_name(self): + """Return the name of the station.""" + try: + return self._details['city']['name'] + except (KeyError, TypeError): + return None + @property def icon(self): """Icon to use in the frontend, if any.""" From 62f26fb701fc4f3bdd14da66cec456da4cb3df93 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Mon, 16 Jan 2017 11:36:35 -0500 Subject: [PATCH 013/191] Fixes #5357 which especify absolute path to save cookie used by USPS sensor. (#5358) Traceback (most recent call last): File "/home/hass/.virtualenvs/home_assistant/lib/python3.5/site-packages/homeassistant/helpers/entity_component.py", line 151, in _async_setup_platform entity_platform.add_entities, discovery_info File "/usr/local/lib/python3.5/asyncio/futures.py", line 361, in __iter__ yield self # This tells Task to wait for completion. File "/usr/local/lib/python3.5/asyncio/tasks.py", line 296, in _wakeup future.result() File "/usr/local/lib/python3.5/asyncio/futures.py", line 274, in result raise self._exception File "/usr/local/lib/python3.5/concurrent/futures/thread.py", line 55, in run result = self.fn(*self.args, **self.kwargs) File "/home/hass/.virtualenvs/home_assistant/lib/python3.5/site-packages/homeassistant/components/sensor/usps.py", line 48, in setup_platform add_devices([USPSSensor(session, config.get(CONF_UPDATE_INTERVAL))]) File "/home/hass/.virtualenvs/home_assistant/lib/python3.5/site-packages/homeassistant/components/sensor/usps.py", line 58, in __init__ self._profile = myusps.get_profile(session) File "/home/hass/.homeassistant/deps/myusps/__init__.py", line 100, in wrapped _login(*args) File "/home/hass/.homeassistant/deps/myusps/__init__.py", line 90, in _login _save_cookies(session.cookies, session.auth.cookie_path) File "/home/hass/.homeassistant/deps/myusps/__init__.py", line 41, in _save_cookies with open(filename, 'wb') as handle: PermissionError: [Errno 13] Permission denied: './usps_cookies.pickle' --- homeassistant/components/sensor/usps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/usps.py b/homeassistant/components/sensor/usps.py index e9562667f6d..0bc7f6cbd5a 100644 --- a/homeassistant/components/sensor/usps.py +++ b/homeassistant/components/sensor/usps.py @@ -22,6 +22,7 @@ REQUIREMENTS = ['myusps==1.0.1'] _LOGGER = logging.getLogger(__name__) +COOKIE = 'usps_cookies.pickle' CONF_UPDATE_INTERVAL = 'update_interval' ICON = 'mdi:package-variant-closed' STATUS_DELIVERED = 'delivered' @@ -39,8 +40,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the USPS platform.""" import myusps try: + cookie = hass.config.path(COOKIE) session = myusps.get_session(config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + config.get(CONF_PASSWORD), + cookie_path=cookie) except myusps.USPSError: _LOGGER.exception('Could not connect to My USPS') return False From 196897fdfc056fe3a10516efcc07e561ca92af3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 16 Jan 2017 18:03:26 +0100 Subject: [PATCH 014/191] Fix python-nest release number (#5369) * fix_#5365 * fix_#5365 --- homeassistant/components/nest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 30a256f1c37..337cc8f9160 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = [ 'http://github.com/technicalpickles/python-nest' '/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip' # nest-cam branch - '#python-nest==3.0.3'] + '#python-nest==3.0.2'] DOMAIN = 'nest' diff --git a/requirements_all.txt b/requirements_all.txt index 6d3f254f02a..9bf5827d8cd 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ hikvision==0.4 # 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/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip#python-nest==3.0.3 +http://github.com/technicalpickles/python-nest/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip#python-nest==3.0.2 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7 From bd3117a0e7c4e70bfee5ea5207b9afc12c35eb45 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 16 Jan 2017 15:56:47 -0500 Subject: [PATCH 015/191] Reserve a test port for broken api to fix race (#5371) * Reserve a test port for broken api to fix race * I cheated. --- tests/test_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_remote.py b/tests/test_remote.py index fa2a53a96cb..d20acc88857 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -23,7 +23,7 @@ HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(MASTER_PORT) HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} -broken_api = remote.API('127.0.0.1', "bladiebla") +broken_api = remote.API('127.0.0.1', "bladybla", port=get_test_instance_port()) hass, slave, master_api = None, None, None From d8560a244cf860c710b2833f4552f6e60375a624 Mon Sep 17 00:00:00 2001 From: Duoxilian Date: Mon, 16 Jan 2017 19:58:34 -0600 Subject: [PATCH 016/191] Made target temperature sensitive to auto mode (#5312) * Made target temperature sensitive to auto mode * Used current_operation instead of operation_mode * When not in auto_mode, the temperature is sent to set_temperature * Low and high targets are switched in the call to set_temperature. * Missed on current_operation. Use STATE_AUTO. * Remove incorrectly checked in directory. * Updated set_temperature based on Martin's feedback. * Use ATTR_TEMPERATURE from const.py --- homeassistant/components/climate/ecobee.py | 69 +++++++++++++++------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index bfb11f703d1..f820d69754d 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -11,10 +11,10 @@ import voluptuous as vol from homeassistant.components import ecobee from homeassistant.components.climate import ( - DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, + DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv @@ -145,12 +145,30 @@ class Thermostat(ClimateDevice): @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" - return int(self.thermostat['runtime']['desiredHeat'] / 10) + if self.current_operation == STATE_AUTO: + return int(self.thermostat['runtime']['desiredHeat'] / 10) + else: + return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" - return int(self.thermostat['runtime']['desiredCool'] / 10) + if self.current_operation == STATE_AUTO: + return int(self.thermostat['runtime']['desiredCool'] / 10) + else: + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return None + if self.current_operation == STATE_HEAT: + return int(self.thermostat['runtime']['desiredHeat'] / 10) + elif self.current_operation == STATE_COOL: + return int(self.thermostat['runtime']['desiredCool'] / 10) + else: + return None @property def desired_fan_mode(self): @@ -246,25 +264,36 @@ class Thermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" - if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \ - kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None: - high_temp = int(kwargs.get(ATTR_TARGET_TEMP_LOW)) - low_temp = int(kwargs.get(ATTR_TARGET_TEMP_HIGH)) + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + temp = kwargs.get(ATTR_TEMPERATURE) + + if self.current_operation == STATE_HEAT and temp is not None: + low_temp = temp + high_temp = temp + 20 + elif self.current_operation == STATE_COOL and temp is not None: + low_temp = temp - 20 + high_temp = temp + if low_temp is None and high_temp is None: + _LOGGER.error( + 'Missing valid arguments for set_temperature in %s', kwargs) + return + + low_temp = int(low_temp) + high_temp = int(high_temp) if self.hold_temp: - self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, - high_temp, "indefinite") - _LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, " - "high=%s, is=%s", low_temp, isinstance( - low_temp, (int, float)), high_temp, - isinstance(high_temp, (int, float))) + self.data.ecobee.set_hold_temp( + self.thermostat_index, high_temp, low_temp, "indefinite") else: - self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, - high_temp) - _LOGGER.debug("Setting ecobee temp to: low=%s, is=%s, " - "high=%s, is=%s", low_temp, isinstance( - low_temp, (int, float)), high_temp, - isinstance(high_temp, (int, float))) + self.data.ecobee.set_hold_temp( + self.thermostat_index, high_temp, low_temp) + + _LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, " + "high=%s, is=%s", low_temp, isinstance( + low_temp, (int, float)), high_temp, + isinstance(high_temp, (int, float))) + self.update_without_throttle = True def set_operation_mode(self, operation_mode): From 0bbb16626c8c70027e2d9e649e1ba91dca775af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 17 Jan 2017 06:52:12 +0100 Subject: [PATCH 017/191] fix bug in flux_led (#5373) --- homeassistant/components/light/flux_led.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 22dd40b30ef..46eec35724a 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -72,6 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): continue device['name'] = device['id'] + " " + ipaddr device[ATTR_MODE] = 'rgbw' + device[CONF_PROTOCOL] = None light = FluxLight(device) if light.is_valid: lights.append(light) From 7511a5842dad21ec517cdad433acdd904941933d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Jan 2017 22:08:47 -0800 Subject: [PATCH 018/191] Fix load_yaml default value (#5383) --- homeassistant/util/yaml.py | 2 +- tests/util/test_yaml.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 64b63b31ca6..00508862279 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -60,7 +60,7 @@ def load_yaml(fname: str) -> Union[List, Dict]: with open(fname, encoding='utf-8') as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict - return yaml.load(conf_file, Loader=SafeLineLoader) or {} + return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() except yaml.YAMLError as exc: _LOGGER.error(exc) raise HomeAssistantError(exc) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 3305fbea6c9..79fd994ce86 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -74,6 +74,12 @@ class TestYaml(unittest.TestCase): doc = yaml.yaml.safe_load(file) assert doc["key"] == "value" + with patch_yaml_files({'test.yaml': None}): + conf = 'key: !include test.yaml' + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc["key"] == {} + @patch('homeassistant.util.yaml.os.walk') def test_include_dir_list(self, mock_walk): """Test include dir list yaml.""" From 51dcd3de6d00a68d3b3586d232068b0d12e1a995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 17 Jan 2017 07:35:09 +0100 Subject: [PATCH 019/191] fix bug #5374 --- homeassistant/components/notify/facebook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/facebook.py b/homeassistant/components/notify/facebook.py index 2acabcf02c0..e598b0e818b 100644 --- a/homeassistant/components/notify/facebook.py +++ b/homeassistant/components/notify/facebook.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def get_service(hass, config): +def get_service(hass, config, discovery_info=None): """Get the Facebook notification service.""" return FacebookNotificationService(config[CONF_PAGE_ACCESS_TOKEN]) From 59f74896a097a53d523fb1f4cce87d6daa0e99ac Mon Sep 17 00:00:00 2001 From: Tom Dickman Date: Tue, 17 Jan 2017 00:45:44 -0600 Subject: [PATCH 020/191] Updated abreviation for miles in darksky sensor (#5382) --- homeassistant/components/sensor/darksky.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 173990a6a2f..ab79cff2aad 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -38,7 +38,7 @@ SENSOR_TYPES = { 'daily_summary': ['Daily Summary', None, None, None, None, None, None], 'icon': ['Icon', None, None, None, None, None, None], 'nearest_storm_distance': ['Nearest Storm Distance', - 'km', 'm', 'km', 'km', 'm', + 'km', 'mi', 'km', 'km', 'mi', 'mdi:weather-lightning'], 'nearest_storm_bearing': ['Nearest Storm Bearing', '°', '°', '°', '°', '°', @@ -63,7 +63,7 @@ SENSOR_TYPES = { 'humidity': ['Humidity', '%', '%', '%', '%', '%', 'mdi:water-percent'], 'pressure': ['Pressure', 'mbar', 'mbar', 'mbar', 'mbar', 'mbar', 'mdi:gauge'], - 'visibility': ['Visibility', 'km', 'm', 'km', 'km', 'm', 'mdi:eye'], + 'visibility': ['Visibility', 'km', 'mi', 'km', 'km', 'mi', 'mdi:eye'], 'ozone': ['Ozone', 'DU', 'DU', 'DU', 'DU', 'DU', 'mdi:eye'], 'apparent_temperature_max': ['Daily High Apparent Temperature', '°C', '°F', '°C', '°C', '°C', From 8e17bf43e0e1e136845b74bf451288b197b0a4f3 Mon Sep 17 00:00:00 2001 From: Bryce Edwards Date: Tue, 17 Jan 2017 00:52:21 -0600 Subject: [PATCH 021/191] added upnp_bind_multicast option to emulated_hue component (#5381) --- homeassistant/components/emulated_hue/__init__.py | 11 ++++++++++- homeassistant/components/emulated_hue/upnp.py | 8 ++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 2efce06528d..d412a7af91f 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -26,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_HOST_IP = 'host_ip' CONF_LISTEN_PORT = 'listen_port' +CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' @@ -35,6 +36,7 @@ TYPE_ALEXA = 'alexa' TYPE_GOOGLE = 'google_home' DEFAULT_LISTEN_PORT = 8300 +DEFAULT_UPNP_BIND_MULTICAST = True DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene'] DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ @@ -47,6 +49,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_HOST_IP): cv.string, vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean, vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list, vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, @@ -84,7 +87,8 @@ def setup(hass, yaml_config): server.register_view(HueOneLightChangeView(config)) upnp_listener = UPNPResponderThread( - config.host_ip_addr, config.listen_port) + config.host_ip_addr, config.listen_port, + config.upnp_bind_multicast) @asyncio.coroutine def stop_emulated_hue_bridge(event): @@ -134,6 +138,11 @@ class Config(object): _LOGGER.warning('When targetting Google Home, listening port has ' 'to be port 80') + # Get whether or not UPNP binds to multicast address (239.255.255.250) + # or to the unicast address (host_ip_addr) + self.upnp_bind_multicast = conf.get( + CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST) + # Get domains that cause both "on" and "off" commands to map to "on" # This is primarily useful for things like scenes or scripts, which # don't really have a concept of being off diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index fd880c40e6e..de3be34e2de 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -60,12 +60,13 @@ class UPNPResponderThread(threading.Thread): _interrupted = False - def __init__(self, host_ip_addr, listen_port): + def __init__(self, host_ip_addr, listen_port, upnp_bind_multicast): """Initialize the class.""" threading.Thread.__init__(self) self.host_ip_addr = host_ip_addr self.listen_port = listen_port + self.upnp_bind_multicast = upnp_bind_multicast # Note that the double newline at the end of # this string is required per the SSDP spec @@ -116,7 +117,10 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 socket.inet_aton("239.255.255.250") + socket.inet_aton(self.host_ip_addr)) - ssdp_socket.bind(("239.255.255.250", 1900)) + if self.upnp_bind_multicast: + ssdp_socket.bind(("239.255.255.250", 1900)) + else: + ssdp_socket.bind((self.host_ip_addr, 1900)) while True: if self._interrupted: From bae38ac17b29097b0b8ce9262fa232c5d91e0165 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 17 Jan 2017 01:52:53 -0500 Subject: [PATCH 022/191] Include .ignore file for search utilities (#5290) * Include .ignore file for search utilities This instructs search utilities to ignore generated html/js files. * Panels has no js files --- .ignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .ignore diff --git a/.ignore b/.ignore new file mode 100644 index 00000000000..45c6dc5561f --- /dev/null +++ b/.ignore @@ -0,0 +1,6 @@ +# Patterns matched in this file will be ignored by supported search utilities + +# Ignore generated html and javascript files +/homeassistant/components/frontend/www_static/*.html +/homeassistant/components/frontend/www_static/*.js +/homeassistant/components/frontend/www_static/panels/*.html From d62b1fc8085d2d58a4e976a5c74833c9c6f97cf7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 17 Jan 2017 07:53:34 +0100 Subject: [PATCH 023/191] Make initial flux update directly when turning on (#5266) --- homeassistant/components/switch/flux.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 9fccf75ea4f..75ecc30c823 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -134,6 +134,8 @@ class FluxSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn on flux.""" + if not self._state: # make initial update + self.flux_update() self._state = True self.unsub_tracker = track_utc_time_change(self.hass, self.flux_update, second=[0, 30]) From 65bf30643af56bc81a6be665cb3ca6b14dd67e66 Mon Sep 17 00:00:00 2001 From: Tom Dickman Date: Tue, 17 Jan 2017 00:55:05 -0600 Subject: [PATCH 024/191] Use timezone aware timestamp in flux_update (#5378) --- homeassistant/components/switch/flux.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 75ecc30c823..8f3ff769a06 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -14,12 +14,11 @@ from homeassistant.components.light import is_on, turn_on from homeassistant.components.sun import next_setting, next_rising from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import CONF_NAME, CONF_PLATFORM -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.event import track_time_change from homeassistant.util.color import ( color_temperature_to_rgb, color_RGB_to_xy, color_temperature_kelvin_to_mired, HASS_COLOR_MIN, HASS_COLOR_MAX) from homeassistant.util.dt import now as dt_now -from homeassistant.util.dt import as_local import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['sun', 'light'] @@ -137,8 +136,8 @@ class FluxSwitch(SwitchDevice): if not self._state: # make initial update self.flux_update() self._state = True - self.unsub_tracker = track_utc_time_change(self.hass, self.flux_update, - second=[0, 30]) + self.unsub_tracker = track_time_change(self.hass, self.flux_update, + second=[0, 30]) self.schedule_update_ha_state() def turn_off(self, **kwargs): @@ -199,8 +198,7 @@ class FluxSwitch(SwitchDevice): _LOGGER.info("Lights updated to x:%s y:%s brightness:%s, %s%%" " of %s cycle complete at %s", x_val, y_val, brightness, round( - percentage_complete * 100), time_state, - as_local(now)) + percentage_complete * 100), time_state, now) else: # Convert to mired and clamp to allowed values mired = color_temperature_kelvin_to_mired(temp) @@ -208,8 +206,7 @@ class FluxSwitch(SwitchDevice): set_lights_temp(self.hass, self._lights, mired, brightness) _LOGGER.info("Lights updated to mired:%s brightness:%s, %s%%" " of %s cycle complete at %s", mired, brightness, - round(percentage_complete * 100), - time_state, as_local(now)) + round(percentage_complete * 100), time_state, now) def find_start_time(self, now): """Return sunrise or start_time if given.""" From 41a6c35ea26ec6cac2e3c122aea3ab0961fbce7d Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Tue, 17 Jan 2017 00:55:42 -0600 Subject: [PATCH 025/191] Install phantomjs in Docker container (#5368) --- Dockerfile | 2 +- script/install_phantomjs | 15 +++++++++++++++ script/setup_docker_prereqs | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 script/install_phantomjs diff --git a/Dockerfile b/Dockerfile index 7522ca9cb64..ecdbbafba66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Copy build scripts -COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/ +COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/install_phantomjs script/ RUN script/setup_docker_prereqs # Install hass component dependencies diff --git a/script/install_phantomjs b/script/install_phantomjs new file mode 100755 index 00000000000..178dfad540e --- /dev/null +++ b/script/install_phantomjs @@ -0,0 +1,15 @@ +#!/bin/bash +# Sets up phantomjs to be used with Home Assistant. + +# Stop on errors +set -e + +PHANTOMJS_VERSION="2.1.1" + +cd "$(dirname "$0")/.." +mkdir -p build && cd build + +curl -LSO https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 +tar -xjf phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 +mv phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin/phantomjs /usr/bin/phantomjs +/usr/bin/phantomjs -v diff --git a/script/setup_docker_prereqs b/script/setup_docker_prereqs index d6ec2789c80..f0c6ddf4cc5 100755 --- a/script/setup_docker_prereqs +++ b/script/setup_docker_prereqs @@ -50,6 +50,9 @@ cp -R /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/pyth # Build and install libcec script/build_libcec +# Install phantomjs +script/install_phantomjs + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From b915cf776bfc90ac87c71e374c2bce4625ba1c52 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Tue, 17 Jan 2017 01:57:25 -0500 Subject: [PATCH 026/191] Introduced Amcrest camera sensors and bump Amcrest module version (#5310) * Introduced Amcrest camera sensors * Makes script/gen_requirements_all.py happy * Bump Amcrest version across all components * - Adjusted scan_interval to 10 seconds - Filtering HTTPError and ConnectTimeout exceptions - Removed @Throttle decorator --- .coveragerc | 1 + homeassistant/components/camera/amcrest.py | 2 +- homeassistant/components/sensor/amcrest.py | 142 +++++++++++++++++++++ requirements_all.txt | 3 +- 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/sensor/amcrest.py diff --git a/.coveragerc b/.coveragerc index 506e51a63d8..91d7e64e79b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -262,6 +262,7 @@ omit = homeassistant/components/openalpr.py homeassistant/components/remote/harmony.py homeassistant/components/scene/hunterdouglas_powerview.py + homeassistant/components/sensor/amcrest.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py homeassistant/components/sensor/bbox.py diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index a3d8dcf35df..bec760dbe10 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_create_clientsession -REQUIREMENTS = ['amcrest==1.0.0'] +REQUIREMENTS = ['amcrest==1.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py new file mode 100644 index 00000000000..7a41bcc6fe4 --- /dev/null +++ b/homeassistant/components/sensor/amcrest.py @@ -0,0 +1,142 @@ +""" +This component provides HA sensor support for Amcrest IP cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.amcrest/ +""" +from datetime import timedelta +import logging + +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_NAME, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, STATE_UNKNOWN) +from homeassistant.helpers.entity import Entity +import homeassistant.loader as loader + +from requests.exceptions import HTTPError, ConnectTimeout + +REQUIREMENTS = ['amcrest==1.1.0'] + +_LOGGER = logging.getLogger(__name__) + +NOTIFICATION_ID = 'amcrest_notification' +NOTIFICATION_TITLE = 'Amcrest Sensor Setup' + +DEFAULT_NAME = 'Amcrest' +DEFAULT_PORT = 80 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) + +# Sensor types are defined like: Name, units, icon +SENSOR_TYPES = { + 'motion_detector': ['Motion Detected', None, 'run'], + 'sdcard': ['SD Used', '%', 'sd'], + 'ptz_preset': ['PTZ Preset', None, 'camera-iris'], +} + +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, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for 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 + except (ConnectTimeout, HTTPError) 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 + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append(AmcrestSensor(config, data, sensor_type)) + + add_devices(sensors, True) + + return True + + +class AmcrestSensor(Entity): + """A sensor implementation for Amcrest IP camera.""" + + def __init__(self, device_info, data, sensor_type): + """Initialize a sensor for Amcrest camera.""" + super(AmcrestSensor, self).__init__() + self._attrs = {} + self._data = data + self._sensor_type = sensor_type + self._name = '{0}_{1}'.format(device_info.get(CONF_NAME), + SENSOR_TYPES.get(self._sensor_type)[0]) + self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) + self._state = STATE_UNKNOWN + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return SENSOR_TYPES.get(self._sensor_type)[1] + + def update(self): + """Get the latest data and updates the state.""" + version, build_date = self._data.camera.software_information + self._attrs['Build Date'] = build_date.split('=')[-1] + self._attrs['Serial Number'] = self._data.camera.serial_number + self._attrs['Version'] = version.split('=')[-1] + + if self._sensor_type == 'motion_detector': + self._state = self._data.camera.is_motion_detected + self._attrs['Record Mode'] = self._data.camera.record_mode + + elif self._sensor_type == 'ptz_preset': + self._state = self._data.camera.ptz_presets_count + + elif self._sensor_type == 'sdcard': + sd_used = self._data.camera.storage_used + sd_total = self._data.camera.storage_total + self._attrs['Total'] = '{0} {1}'.format(*sd_total) + self._attrs['Used'] = '{0} {1}'.format(*sd_used) + self._state = self._data.camera.percent(sd_used[0], sd_total[0]) diff --git a/requirements_all.txt b/requirements_all.txt index 9bf5827d8cd..41acf7fcabd 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,8 @@ TwitterAPI==2.4.3 aiohttp_cors==0.5.0 # homeassistant.components.camera.amcrest -amcrest==1.0.0 +# homeassistant.components.sensor.amcrest +amcrest==1.1.0 # homeassistant.components.apcupsd apcaccess==0.0.4 From ef274c691441a228f461fa394cb07e82d2892191 Mon Sep 17 00:00:00 2001 From: Christiaan Blom Date: Tue, 17 Jan 2017 07:58:38 +0100 Subject: [PATCH 027/191] New Discord notification component (#5330) * Initial commit of discord notification component * Fixed error where script added extra entries to .coveragerc * Cleaned up code * Compliance to PEP8 * removed dependencies * readded dependencies * changed name of client id to token for configuration * Changes for Hound * Incorporated Review Feedback * Review feedback * Updated requirements file * Check compliance --- .coveragerc | 1 + homeassistant/components/notify/discord.py | 51 ++++++++++++++++++++++ requirements_all.txt | 3 ++ 3 files changed, 55 insertions(+) create mode 100644 homeassistant/components/notify/discord.py diff --git a/.coveragerc b/.coveragerc index 91d7e64e79b..f1631b4e99f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -231,6 +231,7 @@ omit = homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py + homeassistant/components/notify/discord.py homeassistant/components/notify/facebook.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py new file mode 100644 index 00000000000..3d426b22645 --- /dev/null +++ b/homeassistant/components/notify/discord.py @@ -0,0 +1,51 @@ +"""Discord platform for notify component.""" +import logging +import asyncio +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['discord.py==0.16.0'] + +CONF_TOKEN = 'token' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Discord notification service.""" + token = config.get(CONF_TOKEN) + return DiscordNotificationService(hass, token) + + +class DiscordNotificationService(BaseNotificationService): + """Implement the notification service for Discord.""" + + def __init__(self, hass, token): + """Initialize the service.""" + self.token = token + self.hass = hass + + @asyncio.coroutine + def async_send_message(self, message, target): + """Login to Discord, send message to channel(s) and log out.""" + import discord + discord_bot = discord.Client(loop=self.hass.loop) + + yield from discord_bot.login(self.token) + + for channelid in target: + channel = discord.Object(id=channelid) + yield from discord_bot.send_message(channel, message) + + yield from discord_bot.logout() + yield from discord_bot.close() + + def send_message(self, message=None, target=None, **kwargs): + """Send a message using Discord.""" + self.hass.async_add_job(self.async_send_message(message, target)) diff --git a/requirements_all.txt b/requirements_all.txt index 41acf7fcabd..4c751e03e1f 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -87,6 +87,9 @@ denonavr==0.3.0 # homeassistant.components.media_player.directv directpy==0.1 +# homeassistant.components.notify.discord +discord.py==0.16.0 + # homeassistant.components.updater distro==1.0.2 From 784b87eb2f551f94536af8ce7495c797438c4610 Mon Sep 17 00:00:00 2001 From: Nick Touran Date: Mon, 16 Jan 2017 23:01:57 -0800 Subject: [PATCH 028/191] Add listing and selection of available MPD playlists (#5237) * Add listing and selection of available MPD playlists through input source UI. * MPD support updating playlist list on the fly as well as at turn-on. * Added no_throttle to force playlist update on mpd power cycle. * Added kwargs signature to get Throttle working right. --- homeassistant/components/media_player/mpd.py | 39 +++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 015ba2fd0ac..2f16410e783 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player.mpd/ """ import logging import socket +from datetime import timedelta import voluptuous as vol @@ -13,11 +14,12 @@ from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, MEDIA_TYPE_PLAYLIST, - MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, MediaPlayerDevice) from homeassistant.const import ( STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD, CONF_HOST) import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle REQUIREMENTS = ['python-mpd2==0.5.5'] @@ -28,9 +30,11 @@ CONF_LOCATION = 'location' DEFAULT_LOCATION = 'MPD' DEFAULT_PORT = 6600 +PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120) + SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -88,6 +92,8 @@ class MpdDevice(MediaPlayerDevice): self.password = password self.status = None self.currentsong = None + self.playlists = [] + self.currentplaylist = None self.client = mpd.MPDClient() self.client.timeout = 10 @@ -100,6 +106,7 @@ class MpdDevice(MediaPlayerDevice): try: self.status = self.client.status() self.currentsong = self.client.currentsong() + self._update_playlists() except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError): # Cleanly disconnect in case connection is not in valid state try: @@ -181,6 +188,20 @@ class MpdDevice(MediaPlayerDevice): """Flag of media commands that are supported.""" return SUPPORT_MPD + @property + def source(self): + """Name of the current input source.""" + return self.currentplaylist + + @property + def source_list(self): + """List of available input sources.""" + return self.playlists + + def select_source(self, source): + """Choose a different available playlist and play it.""" + self.play_media(MEDIA_TYPE_PLAYLIST, source) + def turn_off(self): """Service to send the MPD the command to stop playing.""" self.client.stop() @@ -188,6 +209,14 @@ class MpdDevice(MediaPlayerDevice): def turn_on(self): """Service to send the MPD the command to start playing.""" self.client.play() + self._update_playlists(no_throttle=True) + + @Throttle(PLAYLIST_UPDATE_INTERVAL) + def _update_playlists(self, **kwargs): + """Update available MPD playlists.""" + self.playlists = [] + for playlist_data in self.client.listplaylists(): + self.playlists.append(playlist_data['playlist']) def set_volume_level(self, volume): """Set volume of media player.""" @@ -227,6 +256,12 @@ class MpdDevice(MediaPlayerDevice): """Send the media player the command for playing a playlist.""" _LOGGER.info(str.format("Playing playlist: {0}", media_id)) if media_type == MEDIA_TYPE_PLAYLIST: + if media_id in self.playlists: + self.currentplaylist = media_id + else: + self.currentplaylist = None + _LOGGER.warning(str.format("Unknown playlist name %s.", + media_id)) self.client.clear() self.client.load(media_id) self.client.play() From fd5c2ad08fcb023d342548cec52ef99c39020341 Mon Sep 17 00:00:00 2001 From: Anton Lundin Date: Tue, 17 Jan 2017 08:04:50 +0100 Subject: [PATCH 029/191] Denon improvements (#5251) * denonavr: Expose input as title when in non playing modes Signed-off-by: Anton Lundin * denon: Pop from the intended end of the list Pop from front of list, so we start with NSE0, then NSE1X and so on. Signed-off-by: Anton Lundin * denonavr: Don't provide broken media_url's Only return a media_url if we're in a state that might provide one. Signed-off-by: Anton Lundin * denonavr: Only expose player support when in a player mode This changes so the denonavr only exposes the media player support, when in a mode that supports media playing. Signed-off-by: Anton Lundin --- homeassistant/components/media_player/denon.py | 2 +- .../components/media_player/denonavr.py | 17 ++++++++++++----- requirements_all.txt | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 1feee79635d..22ccd2f0d56 100755 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -150,7 +150,7 @@ class DenonDevice(MediaPlayerDevice): answer_codes = ["NSE0", "NSE1X", "NSE2X", "NSE3X", "NSE4", "NSE5", "NSE6", "NSE7", "NSE8"] for line in self.telnet_request(telnet, 'NSE', all_lines=True): - self._mediainfo += line[len(answer_codes.pop()):] + '\n' + self._mediainfo += line[len(answer_codes.pop(0)):] + '\n' else: self._mediainfo = self.source diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 50b16afc811..e6f0bf99d42 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.3.0'] +REQUIREMENTS = ['denonavr==0.3.1'] _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,9 @@ KEY_DENON_CACHE = 'denonavr_hosts' SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \ + SUPPORT_SELECT_SOURCE | SUPPORT_VOLUME_SET + +SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \ SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY @@ -167,7 +169,10 @@ class DenonDevice(MediaPlayerDevice): @property def supported_media_commands(self): """Flag of media commands that are supported.""" - return SUPPORT_DENON + if self._current_source in self._receiver.netaudio_func_list: + return SUPPORT_DENON | SUPPORT_MEDIA_MODES + else: + return SUPPORT_DENON @property def media_content_id(self): @@ -190,7 +195,7 @@ class DenonDevice(MediaPlayerDevice): @property def media_image_url(self): """Image url of current playing media.""" - if self._power == "ON": + if self._current_source in self._receiver.playing_func_list: return self._media_image_url else: return None @@ -198,7 +203,9 @@ class DenonDevice(MediaPlayerDevice): @property def media_title(self): """Title of current playing media.""" - if self._title is not None: + if self._current_source not in self._receiver.playing_func_list: + return self._current_source + elif self._title is not None: return self._title else: return self._frequency diff --git a/requirements_all.txt b/requirements_all.txt index 4c751e03e1f..7edbb4bcc70 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -82,7 +82,7 @@ colorlog>2.1,<3 concord232==0.14 # homeassistant.components.media_player.denonavr -denonavr==0.3.0 +denonavr==0.3.1 # homeassistant.components.media_player.directv directpy==0.1 From c72f8b1a06338108d779ba6966ad4dc3c3671607 Mon Sep 17 00:00:00 2001 From: Giel Janssens Date: Tue, 17 Jan 2017 08:10:18 +0100 Subject: [PATCH 030/191] Netatmo Presence (#5122) * Netatmo Presence * Travis * Remove def camera_stream --- .../components/binary_sensor/netatmo.py | 151 +++++++++++++----- homeassistant/components/camera/netatmo.py | 45 ++++-- homeassistant/components/netatmo.py | 33 ++-- requirements_all.txt | 2 +- 4 files changed, 160 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 94ef0faaad0..4ef29b9e5f5 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -1,19 +1,19 @@ """ Support for the Netatmo binary sensors. -The binary sensors based on events seen by the NetatmoCamera +The binary sensors based on events seen by the Netatmo cameras. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.netatmo/ +https://home-assistant.io/components/binary_sensor.netatmo/. """ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.components.netatmo import WelcomeData +from homeassistant.components.netatmo import CameraData from homeassistant.loader import get_component -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT +from homeassistant.const import CONF_TIMEOUT, CONF_OFFSET from homeassistant.helpers import config_validation as cv DEPENDENCIES = ["netatmo"] @@ -22,24 +22,37 @@ _LOGGER = logging.getLogger(__name__) # These are the available sensors mapped to binary_sensor class -SENSOR_TYPES = { - "Someone known": 'occupancy', - "Someone unknown": 'motion', - "Motion": 'motion', +WELCOME_SENSOR_TYPES = { + "Someone known": "motion", + "Someone unknown": "motion", + "Motion": "motion", "Tag Vibration": 'vibration', - "Tag Open": 'opening', + "Tag Open": 'opening' +} +PRESENCE_SENSOR_TYPES = { + "Outdoor motion": "motion", + "Outdoor human": "motion", + "Outdoor animal": "motion", + "Outdoor vehicle": "motion" } CONF_HOME = 'home' CONF_CAMERAS = 'cameras' +CONF_WELCOME_SENSORS = 'welcome_sensors' +CONF_PRESENCE_SENSORS = 'presence_sensors' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOME): cv.string, vol.Optional(CONF_TIMEOUT): cv.positive_int, + vol.Optional(CONF_OFFSET): cv.positive_int, vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional( + CONF_WELCOME_SENSORS, default=WELCOME_SENSOR_TYPES.keys()): + vol.All(cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)]), + vol.Optional( + CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES.keys()): + vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]), }) @@ -49,48 +62,68 @@ def setup_platform(hass, config, add_devices, discovery_info=None): netatmo = get_component('netatmo') home = config.get(CONF_HOME, None) timeout = config.get(CONF_TIMEOUT, 15) + offset = config.get(CONF_OFFSET, 90) module_name = None import lnetatmo try: - data = WelcomeData(netatmo.NETATMO_AUTH, home) + data = CameraData(netatmo.NETATMO_AUTH, home) if data.get_camera_names() == []: return None except lnetatmo.NoDevice: return None - sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) + welcome_sensors = config.get( + CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) + presence_sensors = config.get( + CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES) for camera_name in data.get_camera_names(): - if CONF_CAMERAS in config: - if config[CONF_CAMERAS] != [] and \ - camera_name not in config[CONF_CAMERAS]: - continue - for variable in sensors: - if variable in ('Tag Vibration', 'Tag Open'): - continue - add_devices([WelcomeBinarySensor(data, camera_name, module_name, - home, timeout, variable)]) + camera_type = data.get_camera_type(camera=camera_name, home=home) + if camera_type == "NACamera": + if CONF_CAMERAS in config: + if config[CONF_CAMERAS] != [] and \ + camera_name not in config[CONF_CAMERAS]: + continue + for variable in welcome_sensors: + add_devices([NetatmoBinarySensor(data, camera_name, + module_name, home, timeout, + offset, camera_type, + variable)]) + if camera_type == "NOC": + if CONF_CAMERAS in config: + if config[CONF_CAMERAS] != [] and \ + camera_name not in config[CONF_CAMERAS]: + continue + for variable in presence_sensors: + add_devices([NetatmoBinarySensor(data, camera_name, + module_name, home, timeout, + offset, camera_type, + variable)]) for module_name in data.get_module_names(camera_name): - for variable in sensors: + for variable in welcome_sensors: if variable in ('Tag Vibration', 'Tag Open'): - add_devices([WelcomeBinarySensor(data, camera_name, + add_devices([NetatmoBinarySensor(data, camera_name, module_name, home, - timeout, variable)]) + timeout, offset, + camera_type, + variable)]) -class WelcomeBinarySensor(BinarySensorDevice): - """Represent a single binary sensor in a Netatmo Welcome device.""" +class NetatmoBinarySensor(BinarySensorDevice): + """Represent a single binary sensor in a Netatmo Camera device.""" - def __init__(self, data, camera_name, module_name, home, timeout, sensor): + def __init__(self, data, camera_name, module_name, home, + timeout, offset, camera_type, sensor): """Setup for access to the Netatmo camera events.""" self._data = data self._camera_name = camera_name self._module_name = module_name self._home = home self._timeout = timeout + self._offset = offset if home: self._name = home + ' / ' + camera_name else: @@ -99,10 +132,11 @@ class WelcomeBinarySensor(BinarySensorDevice): self._name += ' / ' + module_name self._sensor_name = sensor self._name += ' ' + sensor - camera_id = data.welcomedata.cameraByName(camera=camera_name, + camera_id = data.camera_data.cameraByName(camera=camera_name, home=home)['id'] - self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name, + self._unique_id = "Netatmo_binary_sensor {0} - {1}".format(self._name, camera_id) + self._cameratype = camera_type self.update() @property @@ -118,7 +152,12 @@ class WelcomeBinarySensor(BinarySensorDevice): @property def sensor_class(self): """Return the class of this sensor, from SENSOR_CLASSES.""" - return SENSOR_TYPES.get(self._sensor_name) + if self._cameratype == "NACamera": + return WELCOME_SENSOR_TYPES.get(self._sensor_name) + elif self._cameratype == "NOC": + return PRESENCE_SENSOR_TYPES.get(self._sensor_name) + else: + return None @property def is_on(self): @@ -130,30 +169,54 @@ class WelcomeBinarySensor(BinarySensorDevice): self._data.update() self._data.update_event() - if self._sensor_name == "Someone known": - self._state =\ - self._data.welcomedata.someoneKnownSeen(self._home, + if self._cameratype == "NACamera": + if self._sensor_name == "Someone known": + self._state =\ + self._data.camera_data.someoneKnownSeen(self._home, self._camera_name, self._timeout*60) - elif self._sensor_name == "Someone unknown": - self._state =\ - self._data.welcomedata.someoneUnknownSeen(self._home, + elif self._sensor_name == "Someone unknown": + self._state =\ + self._data.camera_data.someoneUnknownSeen( + self._home, self._camera_name, self._timeout*60) + elif self._sensor_name == "Motion": + self._state =\ + self._data.camera_data.motionDetected(self._home, self._camera_name, self._timeout*60) - elif self._sensor_name == "Motion": - self._state =\ - self._data.welcomedata.motionDetected(self._home, - self._camera_name, - self._timeout*60) + else: + return None + elif self._cameratype == "NOC": + if self._sensor_name == "Outdoor motion": + self._state =\ + self._data.camera_data.outdoormotionDetected( + self._home, self._camera_name, self._offset) + elif self._sensor_name == "Outdoor human": + self._state =\ + self._data.camera_data.humanDetected(self._home, + self._camera_name, + self._offset) + elif self._sensor_name == "Outdoor animal": + self._state =\ + self._data.camera_data.animalDetected(self._home, + self._camera_name, + self._offset) + elif self._sensor_name == "Outdoor vehicle": + self._state =\ + self._data.camera_data.carDetected(self._home, + self._camera_name, + self._offset) + else: + return None elif self._sensor_name == "Tag Vibration": self._state =\ - self._data.welcomedata.moduleMotionDetected(self._home, + self._data.camera_data.moduleMotionDetected(self._home, self._module_name, self._camera_name, self._timeout*60) elif self._sensor_name == "Tag Open": self._state =\ - self._data.welcomedata.moduleOpened(self._home, + self._data.camera_data.moduleOpened(self._home, self._module_name, self._camera_name) else: diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 47808de02b9..563de206dea 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -1,15 +1,15 @@ """ -Support for the Netatmo Welcome camera. +Support for the Netatmo cameras. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.netatmo/ +https://home-assistant.io/components/camera.netatmo/. """ import logging import requests import voluptuous as vol -from homeassistant.components.netatmo import WelcomeData +from homeassistant.components.netatmo import CameraData from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.loader import get_component from homeassistant.helpers import config_validation as cv @@ -30,41 +30,43 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup access to Netatmo Welcome cameras.""" + """Setup access to Netatmo cameras.""" netatmo = get_component('netatmo') home = config.get(CONF_HOME) import lnetatmo try: - data = WelcomeData(netatmo.NETATMO_AUTH, home) + data = CameraData(netatmo.NETATMO_AUTH, home) for camera_name in data.get_camera_names(): + camera_type = data.get_camera_type(camera=camera_name, home=home) if CONF_CAMERAS in config: if config[CONF_CAMERAS] != [] and \ camera_name not in config[CONF_CAMERAS]: continue - add_devices([WelcomeCamera(data, camera_name, home)]) + add_devices([NetatmoCamera(data, camera_name, home, camera_type)]) except lnetatmo.NoDevice: return None -class WelcomeCamera(Camera): - """Representation of the images published from Welcome camera.""" +class NetatmoCamera(Camera): + """Representation of the images published from a Netatmo camera.""" - def __init__(self, data, camera_name, home): + def __init__(self, data, camera_name, home, camera_type): """Setup for access to the Netatmo camera images.""" - super(WelcomeCamera, self).__init__() + super(NetatmoCamera, self).__init__() self._data = data self._camera_name = camera_name if home: self._name = home + ' / ' + camera_name else: self._name = camera_name - camera_id = data.welcomedata.cameraByName(camera=camera_name, + camera_id = data.camera_data.cameraByName(camera=camera_name, home=home)['id'] self._unique_id = "Welcome_camera {0} - {1}".format(self._name, camera_id) - self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls( + self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( camera=camera_name ) + self._cameratype = camera_type def camera_image(self): """Return a still image response from the camera.""" @@ -79,15 +81,30 @@ class WelcomeCamera(Camera): _LOGGER.error('Welcome VPN url changed: %s', error) self._data.update() (self._vpnurl, self._localurl) = \ - self._data.welcomedata.cameraUrls(camera=self._camera_name) + self._data.camera_data.cameraUrls(camera=self._camera_name) return None return response.content @property def name(self): - """Return the name of this Netatmo Welcome device.""" + """Return the name of this Netatmo camera device.""" return self._name + @property + def brand(self): + """Camera brand.""" + return "Netatmo" + + @property + def model(self): + """Camera model.""" + if self._cameratype == "NOC": + return "Presence" + elif self._cameratype == "NACamera": + return "Welcome" + else: + return None + @property def unique_id(self): """Return the unique ID for this sensor.""" diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index b4ebbc1d460..ea691ac94f4 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -18,7 +18,7 @@ from homeassistant.util import Throttle REQUIREMENTS = [ 'https://github.com/jabesq/netatmo-api-python/archive/' - 'v0.8.1.zip#lnetatmo==0.8.1'] + 'v0.9.0.zip#lnetatmo==0.9.0'] _LOGGER = logging.getLogger(__name__) @@ -53,7 +53,8 @@ def setup(hass, config): config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], 'read_station read_camera access_camera ' - 'read_thermostat write_thermostat') + 'read_thermostat write_thermostat ' + 'read_presence access_presence') except HTTPError: _LOGGER.error("Unable to connect to Netatmo API") return False @@ -65,27 +66,28 @@ def setup(hass, config): return True -class WelcomeData(object): +class CameraData(object): """Get the latest data from Netatmo.""" def __init__(self, auth, home=None): """Initialize the data object.""" self.auth = auth - self.welcomedata = None + self.camera_data = None self.camera_names = [] self.module_names = [] self.home = home + self.camera_type = None def get_camera_names(self): """Return all camera available on the API as a list.""" self.camera_names = [] self.update() if not self.home: - for home in self.welcomedata.cameras: - for camera in self.welcomedata.cameras[home].values(): + for home in self.camera_data.cameras: + for camera in self.camera_data.cameras[home].values(): self.camera_names.append(camera['name']) else: - for camera in self.welcomedata.cameras[self.home].values(): + for camera in self.camera_data.cameras[self.home].values(): self.camera_names.append(camera['name']) return self.camera_names @@ -93,20 +95,27 @@ class WelcomeData(object): """Return all module available on the API as a list.""" self.module_names = [] self.update() - cam_id = self.welcomedata.cameraByName(camera=camera_name, + cam_id = self.camera_data.cameraByName(camera=camera_name, home=self.home)['id'] - for module in self.welcomedata.modules.values(): + for module in self.camera_data.modules.values(): if cam_id == module['cam_id']: self.module_names.append(module['name']) return self.module_names + def get_camera_type(self, camera=None, home=None, cid=None): + """Return all module available on the API as a list.""" + for camera_name in self.camera_names: + self.camera_type = self.camera_data.cameraType(camera_name) + return self.camera_type + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" import lnetatmo - self.welcomedata = lnetatmo.WelcomeData(self.auth, size=100) + self.camera_data = lnetatmo.CameraData(self.auth, size=100) @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) def update_event(self): - """Call the Netatmo API to update the list of events.""" - self.welcomedata.updateEvent(home=self.home) + """Call the Netatmo API to update the events.""" + self.camera_data.updateEvent( + home=self.home, cameratype=self.camera_type) diff --git a/requirements_all.txt b/requirements_all.txt index 7edbb4bcc70..31613054a52 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -214,7 +214,7 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9. # https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6 # homeassistant.components.netatmo -https://github.com/jabesq/netatmo-api-python/archive/v0.8.1.zip#lnetatmo==0.8.1 +https://github.com/jabesq/netatmo-api-python/archive/v0.9.0.zip#lnetatmo==0.9.0 # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 From e4a45fa85755ca7abc7374c54d4b793e6caa9763 Mon Sep 17 00:00:00 2001 From: martst Date: Tue, 17 Jan 2017 07:11:02 +0000 Subject: [PATCH 031/191] Improved x10 state monitoring (#5115) * Improved x10 state monitoring * Improved x10 state monitoring * Use update mthod to fetch state change * Use update mthod to fetch state change * Use update function to fetch status * remove temp file * Add doc string to update method --- homeassistant/components/light/x10.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/light/x10.py b/homeassistant/components/light/x10.py index 30ede3eac18..48df8368294 100644 --- a/homeassistant/components/light/x10.py +++ b/homeassistant/components/light/x10.py @@ -33,16 +33,10 @@ def x10_command(command): return check_output(['heyu'] + command.split(' '), stderr=STDOUT) -def get_status(): - """Get on/off status for all x10 units in default housecode.""" - output = check_output('heyu info | grep monitored', shell=True) - return output.decode('utf-8').split(' ')[-1].strip('\n()') - - def get_unit_status(code): """Get on/off status for given unit.""" - unit = int(code[1:]) - return get_status()[16 - int(unit)] == '1' + output = check_output('heyu onstate ' + code, shell=True) + return int(output.decode('utf-8')[0]) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -63,8 +57,8 @@ class X10Light(Light): """Initialize an X10 Light.""" self._name = light['name'] self._id = light['id'] - self._is_on = False self._brightness = 0 + self._state = False @property def name(self): @@ -79,7 +73,7 @@ class X10Light(Light): @property def is_on(self): """Return true if light is on.""" - return self._is_on + return self._state @property def supported_features(self): @@ -90,13 +84,13 @@ class X10Light(Light): """Instruct the light to turn on.""" x10_command('on ' + self._id) self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - self._is_on = True + self._state = True def turn_off(self, **kwargs): """Instruct the light to turn off.""" x10_command('off ' + self._id) - self._is_on = False + self._state = False def update(self): - """Fetch new state data for this light.""" - self._is_on = get_unit_status(self._id) + """Fetch update state.""" + self._state = bool(get_unit_status(self._id)) From 40ba4fd872436e223e060afad5f9bb869b86421e Mon Sep 17 00:00:00 2001 From: Job Vermeulen Date: Tue, 17 Jan 2017 08:15:11 +0100 Subject: [PATCH 032/191] Tado device tracker support (#5046) * Added tado device tracker * Added tado device tracker to .converagerc * Updated docs * Code formatting and removed unused import * Code formatting and removed unused import * Respected the lint line length * Respect pydocstyle rules * Respect the lint line limit length * Fixed reviewer feedback * Changed the tracker to support async * Respect the New line end of file rule * Update .coveragerc --- .coveragerc | 1 + .../components/device_tracker/tado.py | 130 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 homeassistant/components/device_tracker/tado.py diff --git a/.coveragerc b/.coveragerc index f1631b4e99f..3ee6d3cb81a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -168,6 +168,7 @@ omit = homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py + homeassistant/components/device_tracker/tado.py homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/ubus.py diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py new file mode 100644 index 00000000000..5cb1f8fcbd2 --- /dev/null +++ b/homeassistant/components/device_tracker/tado.py @@ -0,0 +1,130 @@ +""" +Support for Tado Smart Thermostat. + +Device tracker platform that supports presence detection. +The detection is based on geofencing enabled devices used with Tado. +""" +import logging +from datetime import timedelta +from collections import namedtuple + +import asyncio +import aiohttp +import async_timeout + +import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import \ + DOMAIN, PLATFORM_SCHEMA, DeviceScanner +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + + +def get_scanner(hass, config): + """Return a Tado scanner.""" + scanner = TadoDeviceScanner(hass, config[DOMAIN]) + + return scanner if scanner.success_init else None + + +Device = namedtuple("Device", ["mac", "name"]) + + +class TadoDeviceScanner(DeviceScanner): + """This class gets geofenced devices from Tado.""" + + def __init__(self, hass, config): + """Initialize the scanner.""" + self.last_results = [] + + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + self.tadoapiurl = 'https://my.tado.com/api/v2/me' \ + '?username={}&password={}' + + self.websession = async_create_clientsession( + hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)) + + self.success_init = self._update_info() + _LOGGER.info("Tado scanner initialized") + + @asyncio.coroutine + def async_scan_devices(self): + """Scan for devices and return a list containing found device ids.""" + yield from self._update_info() + + return [device.mac for device in self.last_results] + + @asyncio.coroutine + def async_get_device_name(self, mac): + """Return the name of the given device or None if we don't know.""" + filter_named = [device.name for device in self.last_results + if device.mac == mac] + + if filter_named: + return filter_named[0] + else: + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Query Tado for device marked as at home. + + Returns boolean if scanning successful. + """ + _LOGGER.debug("Requesting Tado") + + last_results = [] + + response = None + tadojson = None + try: + # get first token + with async_timeout.timeout(10, loop=self.hass.loop): + url = self.tadoapiurl.format(self.username, self.password) + response = yield from self.websession.get( + url + ) + + # error on Tado webservice + if response.status != 200: + _LOGGER.warning( + "Error %d on %s.", response.status, self.tadoapiurl) + self.token = None + return + + tadojson = yield from response.json() + + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.error("Can not load Tado data") + return False + + finally: + if response is not None: + yield from response.release() + + # Find devices that have geofencing enabled, and are currently at home + for mobiledevice in tadojson['mobileDevices']: + if 'location' in mobiledevice: + if mobiledevice['location']['atHome']: + deviceid = mobiledevice['id'] + devicename = mobiledevice['name'] + last_results.append(Device(deviceid, devicename)) + + self.last_results = last_results + + _LOGGER.info("Tado presence query successful") + return True From 915a91dc1b6fdca454c3533765b3e272f504ac4f Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 17 Jan 2017 08:56:00 +0100 Subject: [PATCH 033/191] DSMR: TCP, reconnecting and V4 CRC support (#5164) * Add support for TCP connection. * Implement reconnect logic. * Actually register connect loop task and fix error handling. * Fix lint, configure upstream requirement. * Revert debug logging. * Explicitly catch connection errors. * Test reconnect on setup and reconnect after disconnect. * Style. --- homeassistant/components/sensor/dsmr.py | 79 +++++++++++++--- requirements_all.txt | 2 +- requirements_test.txt | 1 + tests/components/sensor/test_dsmr.py | 120 ++++++++++++++++++++++-- 4 files changed, 177 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 0c42033006c..729b435edbc 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -1,5 +1,4 @@ -""" -Support for Dutch Smart Meter Requirements. +"""Support for Dutch Smart Meter Requirements. Also known as: Smartmeter or P1 port. @@ -24,23 +23,27 @@ 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 from datetime import timedelta +from functools import partial import logging from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) + CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) +from homeassistant.core import CoreState import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import voluptuous as vol _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['dsmr_parser==0.4'] +REQUIREMENTS = ['dsmr_parser==0.6'] CONF_DSMR_VERSION = 'dsmr_version' +CONF_RECONNECT_INTERVAL = 'reconnect_interval' DEFAULT_DSMR_VERSION = '2.2' DEFAULT_PORT = '/dev/ttyUSB0' @@ -51,11 +54,14 @@ ICON_POWER = 'mdi:flash' # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +RECONNECT_INTERVAL = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_HOST, default=None): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( cv.string, vol.In(['4', '2.2'])), + vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, }) @@ -66,7 +72,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): logging.getLogger('dsmr_parser').setLevel(logging.ERROR) from dsmr_parser import obis_references as obis_ref - from dsmr_parser.protocol import create_dsmr_reader + from dsmr_parser.protocol import create_dsmr_reader, create_tcp_dsmr_reader + import serial dsmr_version = config[CONF_DSMR_VERSION] @@ -105,15 +112,55 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 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 factory 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) + if config[CONF_HOST]: + reader_factory = partial(create_tcp_dsmr_reader, + config[CONF_HOST], + config[CONF_PORT], + config[CONF_DSMR_VERSION], + update_entities_telegram, + loop=hass.loop) + else: + reader_factory = partial(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) + @asyncio.coroutine + def connect_and_reconnect(): + """Connect to DSMR and keep reconnecting until HA stops.""" + while hass.state != CoreState.stopping: + # Start DSMR asycnio.Protocol reader + try: + transport, protocol = yield from hass.loop.create_task( + reader_factory()) + except (serial.serialutil.SerialException, ConnectionRefusedError, + TimeoutError): + # log any error while establishing connection and drop to retry + # connection wait + _LOGGER.exception('error connecting to DSMR') + transport = None - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, transport.close) + if transport: + # register listener to close transport on HA shutdown + stop_listerer = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, transport.close) + + # wait for reader to close + yield from protocol.wait_closed() + + if hass.state != CoreState.stopping: + if transport: + # remove listerer + stop_listerer() + + # throttle reconnect attempts + yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL], + loop=hass.loop) + + hass.loop.create_task(connect_and_reconnect()) class DSMREntity(Entity): @@ -187,6 +234,7 @@ class DerivativeDSMREntity(DSMREntity): Gas readings are only reported per hour and don't offer a rate only the current meter reading. This entity converts subsequents readings into a hourly rate. + """ _previous_reading = None @@ -202,10 +250,11 @@ class DerivativeDSMREntity(DSMREntity): def async_update(self): """Recalculate hourly rate if timestamp has changed. - DSMR updates gas meter reading every hour. Along with the - new value a timestamp is provided for the reading. Test - if the last known timestamp differs from the current one - then calculate a new rate for the previous hour. + DSMR updates gas meter reading every hour. Along with the new + value a timestamp is provided for the reading. Test if the last + known timestamp differs from the current one then calculate a + new rate for the previous hour. + """ # check if the timestamp for the object differs from the previous one timestamp = self.get_dsmr_object_attr('datetime') diff --git a/requirements_all.txt b/requirements_all.txt index 31613054a52..2a7e16844f3 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -103,7 +103,7 @@ dnspython3==1.15.0 dovado==0.1.15 # homeassistant.components.sensor.dsmr -dsmr_parser==0.4 +dsmr_parser==0.6 # homeassistant.components.dweet # homeassistant.components.sensor.dweet diff --git a/requirements_test.txt b/requirements_test.txt index d001c5d1a78..3ce07cff7ef 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,3 +16,4 @@ pytest-sugar>=0.7.1 requests_mock>=1.0 mock-open>=1.3.1 flake8-docstrings==1.0.2 +asynctest>=0.8.0 diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 35e224253ee..aae8dfddc5b 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -1,22 +1,51 @@ """Test for DSMR components. -Tests setup of the DSMR component and ensure incoming telegrams cause Entity -to be updated with new values. +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 +import asynctest from homeassistant.bootstrap import async_setup_component from homeassistant.components.sensor.dsmr import DerivativeDSMREntity from homeassistant.const import STATE_UNKNOWN -from tests.common import assert_setup_component, mock_coro +import pytest +from tests.common import assert_setup_component + + +@pytest.fixture +def mock_connection_factory(monkeypatch): + """Mock the create functions for serial and TCP Asyncio connections.""" + from dsmr_parser.protocol import DSMRProtocol + transport = asynctest.Mock(spec=asyncio.Transport) + protocol = asynctest.Mock(spec=DSMRProtocol) + + @asyncio.coroutine + def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + return (transport, protocol) + connection_factory = Mock(wraps=connection_factory) + + # apply the mock to both connection factories + monkeypatch.setattr( + 'dsmr_parser.protocol.create_dsmr_reader', + connection_factory) + monkeypatch.setattr( + 'dsmr_parser.protocol.create_tcp_dsmr_reader', + connection_factory) + + return connection_factory, transport, protocol @asyncio.coroutine -def test_default_setup(hass, monkeypatch): +def test_default_setup(hass, mock_connection_factory): """Test the default setup.""" + (connection_factory, transport, protocol) = mock_connection_factory + from dsmr_parser.obis_references import ( CURRENT_ELECTRICITY_USAGE, ELECTRICITY_ACTIVE_TARIFF, @@ -34,15 +63,11 @@ def test_default_setup(hass, monkeypatch): ]), } - # mock for injecting DSMR telegram - dsmr = Mock(return_value=mock_coro([Mock(), None])) - 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] + telegram_callback = connection_factory.call_args_list[0][0][2] # make sure entities have been created and return 'unknown' state power_consumption = hass.states.get('sensor.power_consumption') @@ -99,3 +124,80 @@ def test_derivative(): 'state should be difference between first and second update' assert entity.unit_of_measurement == 'm3/h' + + +@asyncio.coroutine +def test_tcp(hass, mock_connection_factory): + """If proper config provided TCP connection should be made.""" + (connection_factory, transport, protocol) = mock_connection_factory + + config = { + 'platform': 'dsmr', + 'host': 'localhost', + 'port': 1234, + } + + with assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', + {'sensor': config}) + + assert connection_factory.call_args_list[0][0][0] == 'localhost' + assert connection_factory.call_args_list[0][0][1] == '1234' + + +@asyncio.coroutine +def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): + """Connection should be retried on error during setup.""" + (connection_factory, transport, protocol) = mock_connection_factory + + config = { + 'platform': 'dsmr', + 'reconnect_interval': 0, + } + + # override the mock to have it fail the first time + first_fail_connection_factory = Mock( + wraps=connection_factory, side_effect=[ + TimeoutError]) + + monkeypatch.setattr( + 'dsmr_parser.protocol.create_dsmr_reader', + first_fail_connection_factory) + yield from async_setup_component(hass, 'sensor', {'sensor': config}) + + # wait for sleep to resolve + yield from hass.async_block_till_done() + assert first_fail_connection_factory.call_count == 2, \ + 'connecting not retried' + + +@asyncio.coroutine +def test_reconnect(hass, monkeypatch, mock_connection_factory): + """If transport disconnects, the connection should be retried.""" + (connection_factory, transport, protocol) = mock_connection_factory + config = { + 'platform': 'dsmr', + 'reconnect_interval': 0, + } + + # mock waiting coroutine while connection lasts + closed = asyncio.Event(loop=hass.loop) + + @asyncio.coroutine + def wait_closed(): + yield from closed.wait() + protocol.wait_closed = wait_closed + + yield from async_setup_component(hass, 'sensor', {'sensor': config}) + + assert connection_factory.call_count == 1 + + # indicate disconnect, release wait lock and allow reconnect to happen + closed.set() + # wait for lock set to resolve + yield from hass.async_block_till_done() + # wait for sleep to resolve + yield from hass.async_block_till_done() + + assert connection_factory.call_count == 2, \ + 'connecting not retried' From d240ea56d8e2f199cf82665ff2bcc2c9ab41adae Mon Sep 17 00:00:00 2001 From: anpetrov Date: Tue, 17 Jan 2017 00:01:11 -0800 Subject: [PATCH 034/191] Add Skybeacon BLE temperature/humidity sensor (#5183) Signed-off-by: Andrey Petrov --- .coveragerc | 1 + homeassistant/components/sensor/skybeacon.py | 195 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 199 insertions(+) create mode 100644 homeassistant/components/sensor/skybeacon.py diff --git a/.coveragerc b/.coveragerc index 3ee6d3cb81a..3196003a95a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -321,6 +321,7 @@ omit = homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/serial_pm.py + homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/sma.py homeassistant/components/sensor/snmp.py homeassistant/components/sensor/sonarr.py diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py new file mode 100644 index 00000000000..45dcb5ca2ee --- /dev/null +++ b/homeassistant/components/sensor/skybeacon.py @@ -0,0 +1,195 @@ +""" +Support for SKYBEACON temperature/humidity Bluetooth LE sensor. + +These are inexpensive CR2477-powered ibeacon/eddystone sensors +that come with temperature/sensor module. +More information: http://cnsky9.en.alibaba.com + +example: +sensor: + - platform: skybeacon + mac: 'F7:BE:12:02:47:31' + name: 'living room' +""" + +import logging +import threading +from uuid import UUID + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, + CONF_MAC, + TEMP_CELSIUS, + STATE_UNKNOWN, + EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['pygatt==3.0.0'] + +CONNECT_LOCK = threading.Lock() + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEVICE = 'device' +ATTR_MODEL = 'model' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=""): cv.string, +}) + +BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' +BLE_TEMP_HANDLE = 0x24 +SKIP_HANDLE_LOOKUP = True +CONNECT_TIMEOUT = 30 + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor.""" + name = config.get(CONF_NAME) + mac = config.get(CONF_MAC) + _LOGGER.error("setting up..") + mon = Monitor(hass, mac, name) + add_devices([SkybeaconTemp(name, mon)]) + add_devices([SkybeaconHumid(name, mon)]) + + def monitor_stop(_service_or_event): + """Stop the monitor thread.""" + _LOGGER.info("skybeacon: stopping monitor for %s ", name) + mon.terminate() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, monitor_stop) + mon.start() + + +class SkybeaconHumid(Entity): + """Representation of a humidity sensor.""" + + def __init__(self, name, mon): + """Initialize a sensor.""" + self.mon = mon + self._name = name + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self.mon.data['humid'] + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return "%" + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_DEVICE: "SKYBEACON", + ATTR_MODEL: 1, + } + + +class SkybeaconTemp(Entity): + """Representation of a temperature sensor.""" + + def __init__(self, name, mon): + """Initialize a sensor.""" + self.mon = mon + self._name = name + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self.mon.data['temp'] + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_DEVICE: "SKYBEACON", + ATTR_MODEL: 1, + } + + +class Monitor(threading.Thread): + """Connection handling.""" + + def __init__(self, hass, mac, name): + """Construct interface object.""" + threading.Thread.__init__(self) + self.daemon = False + self.hass = hass + self.mac = mac + self.name = name + self.data = {'temp': STATE_UNKNOWN, 'humid': STATE_UNKNOWN} + self.keep_going = True + self.event = threading.Event() + + def run(self): + """Thread that keeps connection alive.""" + import pygatt + from pygatt.backends import Characteristic + from pygatt.exceptions import (BLEError, + NotConnectedError, + NotificationTimeout) + + cached_char = Characteristic(BLE_TEMP_UUID, BLE_TEMP_HANDLE) + adapter = pygatt.backends.GATTToolBackend() + while True: + try: + _LOGGER.info("connecting to %s", self.name) + # we need concurrent connect, so lets not reset the device + adapter.start(reset_on_start=False) + # seems only one connection can be initiated at a time + with CONNECT_LOCK: + device = adapter.connect(self.mac, + CONNECT_TIMEOUT, + pygatt.BLEAddressType.random) + if SKIP_HANDLE_LOOKUP: + # HACK: inject handle mapping collected offline + # pylint: disable=protected-access + device._characteristics[UUID(BLE_TEMP_UUID)] = cached_char + # magic: writing this makes device happy + device.char_write_handle(0x1b, bytearray([255]), False) + device.subscribe(BLE_TEMP_UUID, self._update) + _LOGGER.info("subscribed to %s", self.name) + while self.keep_going: + # protect against stale connections, just read temperature + device.char_read(BLE_TEMP_UUID, timeout=CONNECT_TIMEOUT) + self.event.wait(60) + break + except (BLEError, NotConnectedError, NotificationTimeout) as ex: + _LOGGER.error("Exception: %s ", str(ex)) + finally: + adapter.stop() + + def _update(self, handle, value): + """Notification callback from pygatt.""" + _LOGGER.info("%s: %15s temperature = %-2d.%-2d, humidity = %3d", + handle, self.name, value[0], value[2], value[1]) + self.data['temp'] = float(("%d.%d" % (value[0], value[2]))) + self.data['humid'] = value[1] + + def terminate(self): + """Signal runner to stop and join thread.""" + self.keep_going = False + self.event.set() + self.join() diff --git a/requirements_all.txt b/requirements_all.txt index 2a7e16844f3..1ebabd8d9e1 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,6 +419,9 @@ pyenvisalink==2.0 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.sensor.skybeacon +pygatt==3.0.0 + # homeassistant.components.remote.harmony pyharmony==1.0.12 From 321a8be33937ccda7bfa9406b7755018a8da315b Mon Sep 17 00:00:00 2001 From: R1chardTM Date: Tue, 17 Jan 2017 09:12:15 +0100 Subject: [PATCH 035/191] Move Nest sensors configuration to Nest component (#4983) * Move Nest sensor config to Nest Component * Ensure Nest Protect sensors are added without specified sensor config * Fix pylint warnings * Remove support for empty monitored condion list * Remove scan interval * Remove scan interval import * Add Nest sensors by default with opt-out --- .../components/binary_sensor/nest.py | 37 +++++------- homeassistant/components/nest.py | 22 ++++++-- homeassistant/components/sensor/nest.py | 56 ++++++------------- homeassistant/const.py | 1 + 4 files changed, 49 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index c66373bc58a..4689bc59082 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -7,14 +7,10 @@ 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.binary_sensor import (BinarySensorDevice) from homeassistant.components.sensor.nest import NestSensor -from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) +from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.components.nest import DATA_NEST -import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['nest'] @@ -42,17 +38,6 @@ _BINARY_TYPES_DEPRECATED = [ _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(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)]) -}) _LOGGER = logging.getLogger(__name__) @@ -63,15 +48,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return nest = hass.data[DATA_NEST] - conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES) - for variable in conf: + # Add all available binary sensors if no Nest binary sensor config is set + if discovery_info == {}: + conditions = _VALID_BINARY_SENSOR_TYPES + else: + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) + + for variable in conditions: 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") + "for valid options.") _LOGGER.error(wstr) sensors = [] @@ -80,16 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): nest.cameras()) for structure, device in device_chain: sensors += [NestBinarySensor(structure, device, variable) - for variable in conf + for variable in conditions if variable in BINARY_TYPES] sensors += [NestBinarySensor(structure, device, variable) - for variable in conf + for variable in conditions if variable in CLIMATE_BINARY_TYPES and device.is_thermostat] if device.is_camera: sensors += [NestBinarySensor(structure, device, variable) - for variable in conf + for variable in conditions if variable in CAMERA_BINARY_TYPES] for activity_zone in device.activity_zones: sensors += [NestActivityZoneSensor(structure, diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 337cc8f9160..13c2ddc7bed 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -11,7 +11,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -from homeassistant.const import (CONF_STRUCTURE, CONF_FILENAME) +from homeassistant.const import (CONF_STRUCTURE, CONF_FILENAME, + CONF_BINARY_SENSORS, CONF_SENSORS, + CONF_MONITORED_CONDITIONS) from homeassistant.loader import get_component _CONFIGURING = {} @@ -30,11 +32,17 @@ NEST_CONFIG_FILE = 'nest.conf' CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list) +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ 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) + vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string), + vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA }) }, extra=vol.ALLOW_EXTRA) @@ -88,9 +96,15 @@ 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) + + sensor_config = conf.get(CONF_SENSORS, {}) + discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + + binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, + binary_sensor_config, config) + _LOGGER.debug("setup done") return True diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index a074dcc310d..6305f5265b0 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -7,15 +7,10 @@ https://home-assistant.io/components/sensor.nest/ from itertools import chain import logging -import voluptuous as vol - -from homeassistant.components.nest import ( - DATA_NEST, DOMAIN) +from homeassistant.components.nest import DATA_NEST from homeassistant.helpers.entity import Entity -from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PLATFORM, - CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS -) +from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_MONITORED_CONDITIONS) DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', @@ -26,23 +21,15 @@ SENSOR_TYPES_DEPRECATED = ['last_ip', 'local_ip', 'last_connection'] -SENSOR_TYPES_DEPRECATED = ['last_ip', - 'local_ip'] - -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'} +SENSOR_UNITS = {'humidity': '%', 'temperature': '°C'} -PROTECT_VARS = ['co_status', - 'smoke_status', - 'battery_health'] +PROTECT_VARS = ['co_status', 'smoke_status', 'battery_health'] PROTECT_VARS_DEPRECATED = ['battery_level'] @@ -51,19 +38,7 @@ SENSOR_TEMP_TYPES = ['temperature', 'target'] _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_WITH_DEPRECATED)] -}) +_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS _LOGGER = logging.getLogger(__name__) @@ -74,9 +49,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return nest = hass.data[DATA_NEST] - conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_SENSOR_TYPES) - for variable in conf: + # Add all available sensors if no Nest sensor config is set + if discovery_info == {}: + conditions = _VALID_SENSOR_TYPES + else: + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) + + for variable in conditions: if variable in _SENSOR_TYPES_DEPRECATED: if variable in DEPRECATED_WEATHER_VARS: wstr = ("Nest no longer provides weather data like %s. See " @@ -87,22 +67,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 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") + "binary_sensor.nest/ for valid options.") _LOGGER.error(wstr) all_sensors = [] for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): sensors = [NestBasicSensor(structure, device, variable) - for variable in conf + for variable in conditions if variable in SENSOR_TYPES and device.is_thermostat] sensors += [NestTempSensor(structure, device, variable) - for variable in conf + for variable in conditions if variable in SENSOR_TEMP_TYPES and device.is_thermostat] sensors += [NestProtectSensor(structure, device, variable) - for variable in conf + for variable in conditions if variable in PROTECT_VARS and device.is_smoke_co_alarm] all_sensors.extend(sensors) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3ef79ad1724..2bdefe6a9fd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -62,6 +62,7 @@ CONF_AUTHENTICATION = 'authentication' CONF_BASE = 'base' CONF_BEFORE = 'before' CONF_BELOW = 'below' +CONF_BINARY_SENSORS = 'binary_sensors' CONF_BLACKLIST = 'blacklist' CONF_BRIGHTNESS = 'brightness' CONF_CODE = 'code' From 298c1654f83a002436b16f465e77333797c5a41a Mon Sep 17 00:00:00 2001 From: Whytey Date: Tue, 17 Jan 2017 18:41:37 +1000 Subject: [PATCH 036/191] New zabbix (#5297) * Hopefully a clean branch for merging * Remove scan_interval, use defaults for now * Fix code style error --- .coveragerc | 3 + homeassistant/components/sensor/zabbix.py | 174 ++++++++++++++++++++++ homeassistant/components/zabbix.py | 60 ++++++++ requirements_all.txt | 3 + 4 files changed, 240 insertions(+) create mode 100644 homeassistant/components/sensor/zabbix.py create mode 100644 homeassistant/components/zabbix.py diff --git a/.coveragerc b/.coveragerc index 3196003a95a..8f67671a5a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -122,6 +122,9 @@ omit = homeassistant/components/mochad.py homeassistant/components/*/mochad.py + homeassistant/components/zabbix.py + homeassistant/components/*/zabbix.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py diff --git a/homeassistant/components/sensor/zabbix.py b/homeassistant/components/sensor/zabbix.py new file mode 100644 index 00000000000..6c3d0a3d653 --- /dev/null +++ b/homeassistant/components/sensor/zabbix.py @@ -0,0 +1,174 @@ +""" +Support for Zabbix Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.zabbix/ +""" +import logging +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +import homeassistant.components.zabbix as zabbix +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['zabbix'] + +_CONF_TRIGGERS = "triggers" +_CONF_HOSTIDS = "hostids" +_CONF_INDIVIDUAL = "individual" +_CONF_NAME = "name" + +_ZABBIX_ID_LIST_SCHEMA = vol.Schema([int]) +_ZABBIX_TRIGGER_SCHEMA = vol.Schema({ + vol.Optional(_CONF_HOSTIDS, default=[]): _ZABBIX_ID_LIST_SCHEMA, + vol.Optional(_CONF_INDIVIDUAL, default=False): cv.boolean(True), + vol.Optional(_CONF_NAME, default=None): cv.string, +}) + +# SCAN_INTERVAL = 30 +# +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(_CONF_TRIGGERS): vol.Any(_ZABBIX_TRIGGER_SCHEMA, None) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Zabbix sensor platform.""" + sensors = [] + + zapi = hass.data[zabbix.DOMAIN] + if not zapi: + _LOGGER.error("zapi is None. Zabbix component hasn't been loaded?") + return False + + _LOGGER.info("Connected to Zabbix API Version %s", + zapi.api_version()) + + trigger_conf = config.get(_CONF_TRIGGERS) + # The following code seems overly complex. Need to think about this... + if trigger_conf: + hostids = trigger_conf.get(_CONF_HOSTIDS) + individual = trigger_conf.get(_CONF_INDIVIDUAL) + name = trigger_conf.get(_CONF_NAME) + + if individual: + # Individual sensor per host + if not hostids: + # We need hostids + _LOGGER.error("If using 'individual', must specify hostids") + return False + + for hostid in hostids: + _LOGGER.debug("Creating Zabbix Sensor: " + str(hostid)) + sensor = ZabbixSingleHostTriggerCountSensor(zapi, + [hostid], + name) + sensors.append(sensor) + else: + if not hostids: + # Single sensor that provides the total count of triggers. + _LOGGER.debug("Creating Zabbix Sensor") + sensor = ZabbixTriggerCountSensor(zapi, name) + else: + # Single sensor that sums total issues for all hosts + _LOGGER.debug("Creating Zabbix Sensor group: " + str(hostids)) + sensor = ZabbixMultipleHostTriggerCountSensor(zapi, + hostids, + name) + sensors.append(sensor) + else: + # Single sensor that provides the total count of triggers. + _LOGGER.debug("Creating Zabbix Sensor") + sensor = ZabbixTriggerCountSensor(zapi) + sensors.append(sensor) + + add_devices(sensors) + + +class ZabbixTriggerCountSensor(Entity): + """Get the active trigger count for all Zabbix monitored hosts.""" + + def __init__(self, zApi, name="Zabbix"): + """Initiate Zabbix sensor.""" + self._name = name + self._zapi = zApi + self._state = None + self._attributes = {} + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return 'issues' + + def _call_zabbix_api(self): + return self._zapi.trigger.get(output="extend", + only_true=1, + monitored=1, + filter={"value": 1}) + + def update(self): + """Update the sensor.""" + _LOGGER.debug("Updating ZabbixTriggerCountSensor: " + str(self._name)) + triggers = self._call_zabbix_api() + self._state = len(triggers) + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._attributes + + +class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor): + """Get the active trigger count for a single Zabbix monitored host.""" + + def __init__(self, zApi, hostid, name=None): + """Initiate Zabbix sensor.""" + super().__init__(zApi, name) + self._hostid = hostid + if not name: + self._name = self._zapi.host.get(hostids=self._hostid, + output="extend")[0]["name"] + + self._attributes["Host ID"] = self._hostid + + def _call_zabbix_api(self): + return self._zapi.trigger.get(hostids=self._hostid, + output="extend", + only_true=1, + monitored=1, + filter={"value": 1}) + + +class ZabbixMultipleHostTriggerCountSensor(ZabbixTriggerCountSensor): + """Get the active trigger count for specified Zabbix monitored hosts.""" + + def __init__(self, zApi, hostids, name=None): + """Initiate Zabbix sensor.""" + super().__init__(zApi, name) + self._hostids = hostids + if not name: + host_names = self._zapi.host.get(hostids=self._hostids, + output="extend") + self._name = " ".join(name["name"] for name in host_names) + self._attributes["Host IDs"] = self._hostids + + def _call_zabbix_api(self): + return self._zapi.trigger.get(hostids=self._hostids, + output="extend", + only_true=1, + monitored=1, + filter={"value": 1}) diff --git a/homeassistant/components/zabbix.py b/homeassistant/components/zabbix.py new file mode 100644 index 00000000000..3418bad6c9c --- /dev/null +++ b/homeassistant/components/zabbix.py @@ -0,0 +1,60 @@ +""" +Support for Zabbix. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zabbix/ +""" +import logging +from urllib.parse import urljoin + +import voluptuous as vol + +from homeassistant.const import ( + CONF_PATH, CONF_HOST, CONF_SSL, CONF_PASSWORD, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyzabbix==0.7.4'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SSL = False +DEFAULT_PATH = "zabbix" + +DOMAIN = 'zabbix' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Zabbix component.""" + from pyzabbix import ZabbixAPI, ZabbixAPIException + + conf = config[DOMAIN] + if conf[CONF_SSL]: + schema = 'https' + else: + schema = 'http' + + url = urljoin('{}://{}'.format(schema, conf[CONF_HOST]), conf[CONF_PATH]) + username = conf.get(CONF_USERNAME, None) + password = conf.get(CONF_PASSWORD, None) + + zapi = ZabbixAPI(url) + try: + zapi.login(username, password) + _LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version()) + except ZabbixAPIException: + _LOGGER.error("Unable to login to the Zabbix API") + return False + + hass.data[DOMAIN] = zapi + return True diff --git a/requirements_all.txt b/requirements_all.txt index 1ebabd8d9e1..d13833efe18 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -542,6 +542,9 @@ pywemo==0.4.9 # homeassistant.components.light.yeelight pyyeelight==1.0-beta +# homeassistant.components.zabbix +pyzabbix==0.7.4 + # homeassistant.components.climate.radiotherm radiotherm==1.2 From d31f00f672eb8361dcb6f9e0dbcd21177017d626 Mon Sep 17 00:00:00 2001 From: Bill Nelson Date: Tue, 17 Jan 2017 13:53:35 -0800 Subject: [PATCH 037/191] Updated Roku IDLE state --- homeassistant/components/media_player/roku.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 5a4e993aee5..728777e5e9e 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -114,7 +114,8 @@ class RokuDevice(MediaPlayerDevice): if self.current_app is None: return STATE_UNKNOWN - if self.current_app.name in ["Power Saver", "Default screensaver"]: + idle_list = ["Power Saver", "Screensaver", "screensaver"] + if any(idle_type in self.current_app.name for idle_type in idle_list): return STATE_IDLE elif self.current_app.name == "Roku": return STATE_HOME From cfc936761b967631f82acbeada3f40f08ea14c95 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Jan 2017 23:35:02 +0100 Subject: [PATCH 038/191] Make upc more robust (#5404) * Make upc more robust * update unittest * add test for parse error --- .../components/device_tracker/upc_connect.py | 15 ++-- .../device_tracker/test_upc_connect.py | 83 +++++++++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index 13336e939a5..2e1a4d7b947 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -74,12 +74,15 @@ class UPCDeviceScanner(DeviceScanner): return [] raw = yield from self._async_ws_function(CMD_DEVICES) - if raw is None: - _LOGGER.warning("Can't read device from %s", self.host) - return - xml_root = ET.fromstring(raw) - return [mac.text for mac in xml_root.iter('MACAddr')] + try: + xml_root = ET.fromstring(raw) + return [mac.text for mac in xml_root.iter('MACAddr')] + except (ET.ParseError, TypeError): + _LOGGER.warning("Can't read device from %s", self.host) + self.token = None + + return [] @asyncio.coroutine def async_get_device_name(self, device): @@ -107,7 +110,7 @@ class UPCDeviceScanner(DeviceScanner): }) # successfull? - if data.find("successful") != -1: + if data is not None: return True return False diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index 728eb104b8b..1bcbc841d3a 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -230,6 +230,50 @@ class TestUPCConnect(object): cookies={'sessionToken': '1235678'} ) + scanner.token = None + mac_list = run_coroutine_threadsafe( + scanner.async_scan_devices(), self.hass.loop).result() + + assert len(aioclient_mock.mock_calls) == 3 + assert aioclient_mock.mock_calls[1][2]['fun'] == 15 + assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', + '70:EE:50:27:A1:38'] + + def test_scan_devices_without_session_wrong_re(self, aioclient_mock): + """Setup a upc platform and scan device with no token and wrong.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(self.host), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(self.host), + content=b'successful', + cookies={'sessionToken': '654321'} + ) + + scanner = run_coroutine_threadsafe(platform.async_get_scanner( + self.hass, {DOMAIN: { + CONF_PLATFORM: 'upc_connect', + CONF_HOST: self.host, + CONF_PASSWORD: '123456' + }} + ), self.hass.loop).result() + + assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' + assert aioclient_mock.mock_calls[1][2]['fun'] == 15 + assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://{}/common_page/login.html".format(self.host), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(self.host), + status=400, + cookies={'sessionToken': '1235678'} + ) + scanner.token = None mac_list = run_coroutine_threadsafe( scanner.async_scan_devices(), self.hass.loop).result() @@ -237,3 +281,42 @@ class TestUPCConnect(object): assert len(aioclient_mock.mock_calls) == 2 assert aioclient_mock.mock_calls[1][2]['fun'] == 15 assert mac_list == [] + + def test_scan_devices_parse_error(self, aioclient_mock): + """Setup a upc platform and scan device with parse error.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(self.host), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(self.host), + content=b'successful', + cookies={'sessionToken': '654321'} + ) + + scanner = run_coroutine_threadsafe(platform.async_get_scanner( + self.hass, {DOMAIN: { + CONF_PLATFORM: 'upc_connect', + CONF_HOST: self.host, + CONF_PASSWORD: '123456' + }} + ), self.hass.loop).result() + + assert aioclient_mock.mock_calls[1][2]['Password'] == '123456' + assert aioclient_mock.mock_calls[1][2]['fun'] == 15 + assert aioclient_mock.mock_calls[1][2]['token'] == '654321' + + aioclient_mock.clear_requests() + aioclient_mock.post( + "http://{}/xml/getter.xml".format(self.host), + text="Blablebla blabalble", + cookies={'sessionToken': '1235678'} + ) + + mac_list = run_coroutine_threadsafe( + scanner.async_scan_devices(), self.hass.loop).result() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2]['fun'] == 123 + assert scanner.token is None + assert mac_list == [] From bfc0a6a17c471d0124310cc92fac9656b6d581df Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Jan 2017 23:40:34 +0100 Subject: [PATCH 039/191] Use constants (#5390) --- homeassistant/components/light/qwikswitch.py | 5 ++- homeassistant/components/qwikswitch.py | 33 +++++++++++-------- homeassistant/components/switch/qwikswitch.py | 6 ++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index 5612f41c942..b963f14cfb4 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -5,8 +5,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.qwikswitch/ """ import logging + import homeassistant.components.qwikswitch as qwikswitch +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['qwikswitch'] @@ -14,7 +17,7 @@ DEPENDENCIES = ['qwikswitch'] def setup_platform(hass, config, add_devices, discovery_info=None): """Add lights from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error('Configure Qwikswitch Component.') + _LOGGER.error("Configure Qwikswitch component") return False add_devices(qwikswitch.QSUSB['light']) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 2e01d91f50f..3c0e66679bc 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -5,26 +5,32 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ import logging + import voluptuous as vol -from homeassistant.const import (EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL) from homeassistant.helpers.discovery import load_platform -from homeassistant.components.light import (ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.switch import SwitchDevice -DOMAIN = 'qwikswitch' REQUIREMENTS = ['pyqwikswitch==0.4'] _LOGGER = logging.getLogger(__name__) +DOMAIN = 'qwikswitch' + +CONF_DIMMER_ADJUST = 'dimmer_adjust' +CONF_BUTTON_EVENTS = 'button_events' CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3)) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required('url', default='http://127.0.0.1:2020'): vol.Coerce(str), - vol.Optional('dimmer_adjust', default=1): CV_DIM_VALUE, - vol.Optional('button_events'): vol.Coerce(str) + vol.Required(CONF_URL, default='http://127.0.0.1:2020'): + vol.Coerce(str), + vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE, + vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str) })}, extra=vol.ALLOW_EXTRA) QSUSB = {} @@ -118,16 +124,17 @@ class QSLight(QSToggleEntity, Light): def setup(hass, config): """Setup the QSUSB component.""" - from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, - PQS_VALUE, PQS_TYPE, QSType) + from pyqwikswitch import ( + QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, PQS_VALUE, PQS_TYPE, + QSType) # Override which cmd's in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] - cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS)) + cmd_buttons = config[DOMAIN].get(CONF_BUTTON_EVENTS, ','.join(CMD_BUTTONS)) cmd_buttons = cmd_buttons.split(',') - url = config[DOMAIN]['url'] - dimmer_adjust = config[DOMAIN]['dimmer_adjust'] + url = config[DOMAIN][CONF_URL] + dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] qsusb = QSUsb(url, _LOGGER, dimmer_adjust) diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index c3adc33deff..7aea1dea1e1 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -5,8 +5,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.qwikswitch/ """ import logging + import homeassistant.components.qwikswitch as qwikswitch +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['qwikswitch'] @@ -14,8 +17,7 @@ DEPENDENCIES = ['qwikswitch'] def setup_platform(hass, config, add_devices, discovery_info=None): """Add switched from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - 'Configure main Qwikswitch component') + _LOGGER.error("Configure Qwikswitch component") return False add_devices(qwikswitch.QSUSB['switch']) From 4c52380519d3e2c683818cf331f862a1c14ba127 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Jan 2017 23:41:09 +0100 Subject: [PATCH 040/191] Sync logger messages with Mi-Flora and link to docs (#5391) --- homeassistant/components/sensor/skybeacon.py | 46 ++++++++------------ 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 45dcb5ca2ee..dd6a117d447 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -1,37 +1,27 @@ """ -Support for SKYBEACON temperature/humidity Bluetooth LE sensor. +Support for Skybeacon temperature/humidity Bluetooth LE sensors. -These are inexpensive CR2477-powered ibeacon/eddystone sensors -that come with temperature/sensor module. -More information: http://cnsky9.en.alibaba.com - -example: -sensor: - - platform: skybeacon - mac: 'F7:BE:12:02:47:31' - name: 'living room' +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.skybeacon/ """ - import logging import threading from uuid import UUID import voluptuous as vol + import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, - CONF_MAC, - TEMP_CELSIUS, - STATE_UNKNOWN, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + CONF_NAME, CONF_MAC, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['pygatt==3.0.0'] -CONNECT_LOCK = threading.Lock() - _LOGGER = logging.getLogger(__name__) +CONNECT_LOCK = threading.Lock() + ATTR_DEVICE = 'device' ATTR_MODEL = 'model' @@ -48,17 +38,18 @@ CONNECT_TIMEOUT = 30 # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor.""" + """Set up the sensor.""" name = config.get(CONF_NAME) mac = config.get(CONF_MAC) - _LOGGER.error("setting up..") + _LOGGER.debug("Setting up...") + mon = Monitor(hass, mac, name) add_devices([SkybeaconTemp(name, mon)]) add_devices([SkybeaconHumid(name, mon)]) def monitor_stop(_service_or_event): """Stop the monitor thread.""" - _LOGGER.info("skybeacon: stopping monitor for %s ", name) + _LOGGER.info("Stopping monitor for %s", name) mon.terminate() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, monitor_stop) @@ -147,15 +138,14 @@ class Monitor(threading.Thread): """Thread that keeps connection alive.""" import pygatt from pygatt.backends import Characteristic - from pygatt.exceptions import (BLEError, - NotConnectedError, - NotificationTimeout) + from pygatt.exceptions import ( + BLEError, NotConnectedError, NotificationTimeout) cached_char = Characteristic(BLE_TEMP_UUID, BLE_TEMP_HANDLE) adapter = pygatt.backends.GATTToolBackend() while True: try: - _LOGGER.info("connecting to %s", self.name) + _LOGGER.info("Connecting to %s", self.name) # we need concurrent connect, so lets not reset the device adapter.start(reset_on_start=False) # seems only one connection can be initiated at a time @@ -170,7 +160,7 @@ class Monitor(threading.Thread): # magic: writing this makes device happy device.char_write_handle(0x1b, bytearray([255]), False) device.subscribe(BLE_TEMP_UUID, self._update) - _LOGGER.info("subscribed to %s", self.name) + _LOGGER.info("Subscribed to %s", self.name) while self.keep_going: # protect against stale connections, just read temperature device.char_read(BLE_TEMP_UUID, timeout=CONNECT_TIMEOUT) @@ -183,8 +173,8 @@ class Monitor(threading.Thread): def _update(self, handle, value): """Notification callback from pygatt.""" - _LOGGER.info("%s: %15s temperature = %-2d.%-2d, humidity = %3d", - handle, self.name, value[0], value[2], value[1]) + _LOGGER.debug("%s: %15s temperature = %-2d.%-2d, humidity = %3d", + handle, self.name, value[0], value[2], value[1]) self.data['temp'] = float(("%d.%d" % (value[0], value[2]))) self.data['humid'] = value[1] From 50b326c7fc01c99c7e963f2676271ccc1f263f44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Jan 2017 21:04:56 -0800 Subject: [PATCH 041/191] Upgrade somecomfort (#5413) --- homeassistant/components/climate/honeywell.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 0d31cdd1387..3387baf76d8 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['evohomeclient==0.2.5', - 'somecomfort==0.3.2'] + 'somecomfort==0.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d13833efe18..85cc320ab1c 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -585,7 +585,7 @@ sleepyq==0.6 snapcast==1.2.2 # homeassistant.components.climate.honeywell -somecomfort==0.3.2 +somecomfort==0.4.1 # homeassistant.components.sensor.speedtest speedtest-cli==1.0.1 From 283bcf367b0923f9c55dee6be9d2b3356c168b29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Jan 2017 21:48:33 -0800 Subject: [PATCH 042/191] Ignore python-eq3bt from auto-building --- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 85cc320ab1c..b8f93caa5b0 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -483,7 +483,7 @@ pysnmp==4.3.2 python-digitalocean==1.10.1 # homeassistant.components.climate.eq3btsmart -python-eq3bt==0.1.4 +# python-eq3bt==0.1.4 # homeassistant.components.sensor.darksky python-forecastio==1.3.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0231e0d5177..81fb17aac17 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -19,6 +19,7 @@ COMMENT_REQUIREMENTS = ( 'pyuserinput', 'evdev', 'pycups', + 'python-eq3bt', ) IGNORE_PACKAGES = ( From 6cd57ac02f33e552eeaadfde5d1f71b125686a8c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Jan 2017 21:53:03 -0800 Subject: [PATCH 043/191] Fix Yamaha doing I/O in event loop (#5387) --- homeassistant/components/media_player/yamaha.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 2596e7a4ca9..84778cef2d5 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -103,6 +103,7 @@ class YamahaDevice(MediaPlayerDevice): self._source_ignore = source_ignore or [] self._source_names = source_names or {} self._reverse_mapping = None + self._playback_support = None self._is_playback_supported = False self._play_status = None self.update() @@ -131,6 +132,7 @@ class YamahaDevice(MediaPlayerDevice): current_source = self._receiver.input self._current_source = self._source_names.get( current_source, current_source) + self._playback_support = self._receiver.get_playback_support() self._is_playback_supported = self._receiver.is_playback_supported( self._current_source) @@ -183,7 +185,7 @@ class YamahaDevice(MediaPlayerDevice): """Flag of media commands that are supported.""" supported_commands = SUPPORT_YAMAHA - supports = self._receiver.get_playback_support() + supports = self._playback_support mapping = {'play': (SUPPORT_PLAY | SUPPORT_PLAY_MEDIA), 'pause': SUPPORT_PAUSE, 'stop': SUPPORT_STOP, From f7ac644c11557905ae7da7a2882ed5d792f8d2a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Jan 2017 22:00:15 -0800 Subject: [PATCH 044/191] Make SMTP tests fast --- tests/components/notify/test_smtp.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 6a2f8c7acbf..509310099e3 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -1,5 +1,6 @@ """The tests for the notify smtp platform.""" import unittest +from unittest.mock import patch from homeassistant.components.notify import smtp @@ -9,10 +10,6 @@ from tests.common import get_test_home_assistant class MockSMTP(smtp.MailNotificationService): """Test SMTP object that doesn't need a working server.""" - def connection_is_valid(self): - """Pretend connection is always valid for testing.""" - return True - def _send_email(self, msg): """Just return string for testing.""" return msg.as_string() @@ -31,7 +28,8 @@ class TestNotifySmtp(unittest.TestCase): """"Stop down everything that was started.""" self.hass.stop() - def test_text_email(self): + @patch('email.utils.make_msgid', return_value='') + def test_text_email(self, mock_make_msgid): """Test build of default text email behavior.""" msg = self.mailer.send_message('Test msg') expected = ('^Content-Type: text/plain; charset="us-ascii"\n' @@ -47,7 +45,8 @@ class TestNotifySmtp(unittest.TestCase): 'Test msg$') self.assertRegex(msg, expected) - def test_mixed_email(self): + @patch('email.utils.make_msgid', return_value='') + def test_mixed_email(self, mock_make_msgid): """Test build of mixed text email behavior.""" msg = self.mailer.send_message('Test msg', data={'images': ['test.jpg']}) From 2a362fd1ff3b3b614e47bd22d086c6cf4a67f762 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 18 Jan 2017 07:08:03 +0100 Subject: [PATCH 045/191] Asyncio notify component migration (#5377) * Async migrate notify/platform * convert group to async * fix unittest --- homeassistant/components/notify/__init__.py | 116 +++++++++++++------- homeassistant/components/notify/discord.py | 10 +- homeassistant/components/notify/group.py | 25 +++-- tests/components/notify/test_apns.py | 8 +- tests/components/notify/test_demo.py | 34 +++--- tests/components/notify/test_group.py | 31 ++++-- 6 files changed, 136 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index a5c1e53ef03..d1d35e07054 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -4,13 +4,15 @@ Provides functionality to notify people. For more details about this component, please refer to the documentation at https://home-assistant.io/components/notify/ """ +import asyncio import logging import os from functools import partial import voluptuous as vol -import homeassistant.bootstrap as bootstrap +from homeassistant.bootstrap import async_prepare_setup_platform +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_NAME, CONF_PLATFORM @@ -64,91 +66,110 @@ def send_message(hass, message, title=None, data=None): hass.services.call(DOMAIN, SERVICE_NOTIFY, info) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup the notify services.""" - descriptions = load_yaml_config_file( + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join(os.path.dirname(__file__), 'services.yaml')) targets = {} - def setup_notify_platform(platform, p_config=None, discovery_info=None): + @asyncio.coroutine + def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a notify platform.""" if p_config is None: p_config = {} if discovery_info is None: discovery_info = {} - notify_implementation = bootstrap.prepare_setup_platform( - hass, config, DOMAIN, platform) + platform = yield from async_prepare_setup_platform( + hass, config, DOMAIN, p_type) - if notify_implementation is None: + if platform is None: _LOGGER.error("Unknown notification service specified") - return False + return - notify_service = notify_implementation.get_service( - hass, p_config, discovery_info) + _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) + notify_service = None + try: + if hasattr(platform, 'async_get_service'): + notify_service = yield from \ + platform.async_get_service(hass, p_config, discovery_info) + elif hasattr(platform, 'get_service'): + notify_service = yield from hass.loop.run_in_executor( + None, platform.get_service, hass, p_config, discovery_info) + else: + raise HomeAssistantError("Invalid notify platform.") - if notify_service is None: - _LOGGER.error("Failed to initialize notification service %s", - platform) - return False + if notify_service is None: + _LOGGER.error( + "Failed to initialize notification service %s", p_type) + return - def notify_message(notify_service, call): + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error setting up platform %s', p_type) + return + + notify_service.hass = hass + + @asyncio.coroutine + def async_notify_message(service): """Handle sending notification message service calls.""" kwargs = {} - message = call.data[ATTR_MESSAGE] - title = call.data.get(ATTR_TITLE) + message = service.data[ATTR_MESSAGE] + title = service.data.get(ATTR_TITLE) if title: title.hass = hass - kwargs[ATTR_TITLE] = title.render() + kwargs[ATTR_TITLE] = title.async_render() - if targets.get(call.service) is not None: - kwargs[ATTR_TARGET] = [targets[call.service]] - elif call.data.get(ATTR_TARGET) is not None: - kwargs[ATTR_TARGET] = call.data.get(ATTR_TARGET) + if targets.get(service.service) is not None: + kwargs[ATTR_TARGET] = [targets[service.service]] + elif service.data.get(ATTR_TARGET) is not None: + kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) message.hass = hass - kwargs[ATTR_MESSAGE] = message.render() - kwargs[ATTR_DATA] = call.data.get(ATTR_DATA) + kwargs[ATTR_MESSAGE] = message.async_render() + kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) - notify_service.send_message(**kwargs) - - service_call_handler = partial(notify_message, notify_service) + yield from notify_service.async_send_message(**kwargs) if hasattr(notify_service, 'targets'): platform_name = ( p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or - platform) + p_type) for name, target in notify_service.targets.items(): target_name = slugify('{}_{}'.format(platform_name, name)) targets[target_name] = target - hass.services.register(DOMAIN, target_name, - service_call_handler, - descriptions.get(SERVICE_NOTIFY), - schema=NOTIFY_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, target_name, async_notify_message, + descriptions.get(SERVICE_NOTIFY), + schema=NOTIFY_SERVICE_SCHEMA) platform_name = ( p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or SERVICE_NOTIFY) platform_name_slug = slugify(platform_name) - hass.services.register( - DOMAIN, platform_name_slug, service_call_handler, + hass.services.async_register( + DOMAIN, platform_name_slug, async_notify_message, descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA) return True - for platform, p_config in config_per_platform(config, DOMAIN): - if not setup_notify_platform(platform, p_config): - _LOGGER.error("Failed to set up platform %s", platform) - continue + setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config + in config_per_platform(config, DOMAIN)] - def platform_discovered(platform, info): + if setup_tasks: + yield from asyncio.wait(setup_tasks, loop=hass.loop) + + @asyncio.coroutine + def async_platform_discovered(platform, info): """Callback to load a platform.""" - setup_notify_platform(platform, discovery_info=info) + yield from async_setup_platform(platform, discovery_info=info) - discovery.listen_platform(hass, DOMAIN, platform_discovered) + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) return True @@ -156,9 +177,20 @@ def setup(hass, config): class BaseNotificationService(object): """An abstract class for notification services.""" + hass = None + def send_message(self, message, **kwargs): """Send a message. kwargs can contain ATTR_TITLE to specify a title. """ - raise NotImplementedError + raise NotImplementedError() + + def async_send_message(self, message, **kwargs): + """Send a message. + + kwargs can contain ATTR_TITLE to specify a title. + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, partial(self.send_message, message, **kwargs)) diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index 3d426b22645..34fb2a1770a 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -4,7 +4,7 @@ import asyncio import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - PLATFORM_SCHEMA, BaseNotificationService) + PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET) _LOGGER = logging.getLogger(__name__) @@ -32,20 +32,16 @@ class DiscordNotificationService(BaseNotificationService): self.hass = hass @asyncio.coroutine - def async_send_message(self, message, target): + def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" import discord discord_bot = discord.Client(loop=self.hass.loop) yield from discord_bot.login(self.token) - for channelid in target: + for channelid in kwargs[ATTR_TARGET]: channel = discord.Object(id=channelid) yield from discord_bot.send_message(channel, message) yield from discord_bot.logout() yield from discord_bot.close() - - def send_message(self, message=None, target=None, **kwargs): - """Send a message using Discord.""" - self.hass.async_add_job(self.async_send_message(message, target)) diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index 3de79f5a7be..07cc7b1146a 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -4,15 +4,15 @@ Group platform for notify component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.group/ """ +import asyncio import collections from copy import deepcopy import logging import voluptuous as vol from homeassistant.const import ATTR_SERVICE -from homeassistant.components.notify import (DOMAIN, ATTR_MESSAGE, ATTR_DATA, - PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import ( + DOMAIN, ATTR_MESSAGE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def update(input_dict, update_source): - """Deep update a dictionary.""" + """Deep update a dictionary. + + Async friendly. + """ for key, val in update_source.items(): if isinstance(val, collections.Mapping): recurse = update(input_dict.get(key, {}), val) @@ -38,7 +41,8 @@ def update(input_dict, update_source): return input_dict -def get_service(hass, config, discovery_info=None): +@asyncio.coroutine +def async_get_service(hass, config, discovery_info=None): """Get the Group notification service.""" return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) @@ -51,14 +55,19 @@ class GroupNotifyPlatform(BaseNotificationService): self.hass = hass self.entities = entities - def send_message(self, message="", **kwargs): + @asyncio.coroutine + def async_send_message(self, message="", **kwargs): """Send message to all entities in the group.""" payload = {ATTR_MESSAGE: message} payload.update({key: val for key, val in kwargs.items() if val}) + tasks = [] for entity in self.entities: sending_payload = deepcopy(payload.copy()) if entity.get(ATTR_DATA) is not None: update(sending_payload, entity.get(ATTR_DATA)) - self.hass.services.call(DOMAIN, entity.get(ATTR_SERVICE), - sending_payload) + tasks.append(self.hass.services.async_call( + DOMAIN, entity.get(ATTR_SERVICE), sending_payload)) + + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 6949863280c..7246aea3302 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -41,7 +41,9 @@ class TestApns(unittest.TestCase): assert setup_component(self.hass, notify.DOMAIN, CONFIG) assert handle_config[notify.DOMAIN] - def test_apns_setup_full(self): + @patch('os.path.isfile', return_value=True) + @patch('os.access', return_value=True) + def test_apns_setup_full(self, mock_access, mock_isfile): """Test setup with all data.""" config = { 'notify': { @@ -53,7 +55,9 @@ class TestApns(unittest.TestCase): } } - self.assertTrue(notify.setup(self.hass, config)) + with assert_setup_component(1) as handle_config: + assert setup_component(self.hass, notify.DOMAIN, config) + assert handle_config[notify.DOMAIN] def test_apns_setup_missing_name(self): """Test setup with missing name.""" diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 1ccb3f5c56d..de13f678ae0 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -1,4 +1,5 @@ """The tests for the notify demo platform.""" +import asyncio import unittest from unittest.mock import patch @@ -16,6 +17,12 @@ CONFIG = { } +@asyncio.coroutine +def mock_setup_platform(): + """Mock prepare_setup_platform.""" + return None + + class TestNotifyDemo(unittest.TestCase): """Test the demo notify.""" @@ -45,23 +52,16 @@ class TestNotifyDemo(unittest.TestCase): """Test setup.""" self._setup_notify() - @patch('homeassistant.bootstrap.prepare_setup_platform') + @patch('homeassistant.bootstrap.async_prepare_setup_platform', + return_value=mock_setup_platform()) def test_no_prepare_setup_platform(self, mock_prep_setup_platform): """Test missing notify platform.""" - mock_prep_setup_platform.return_value = None - with self.assertLogs('homeassistant.components.notify', - level='ERROR') as log_handle: - self._setup_notify() - self.hass.block_till_done() - assert mock_prep_setup_platform.called - self.assertEqual( - log_handle.output, - ['ERROR:homeassistant.components.notify:' - 'Unknown notification service specified', - 'ERROR:homeassistant.components.notify:' - 'Failed to set up platform demo']) + with assert_setup_component(0): + setup_component(self.hass, notify.DOMAIN, CONFIG) - @patch('homeassistant.components.notify.demo.get_service') + assert mock_prep_setup_platform.called + + @patch('homeassistant.components.notify.demo.get_service', autospec=True) def test_no_notify_service(self, mock_demo_get_service): """Test missing platform notify service instance.""" mock_demo_get_service.return_value = None @@ -73,11 +73,9 @@ class TestNotifyDemo(unittest.TestCase): self.assertEqual( log_handle.output, ['ERROR:homeassistant.components.notify:' - 'Failed to initialize notification service demo', - 'ERROR:homeassistant.components.notify:' - 'Failed to set up platform demo']) + 'Failed to initialize notification service demo']) - @patch('homeassistant.components.notify.demo.get_service') + @patch('homeassistant.components.notify.demo.get_service', autospec=True) def test_discover_notify(self, mock_demo_get_service): """Test discovery of notify demo platform.""" assert notify.DOMAIN not in self.hass.config.components diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index 14c8c46b6c3..1aa07fed583 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from homeassistant.bootstrap import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import group, demo +from homeassistant.util.async import run_coroutine_threadsafe from tests.common import assert_setup_component, get_test_home_assistant @@ -16,8 +17,11 @@ class TestNotifyGroup(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.events = [] - self.service1 = MagicMock() - self.service2 = MagicMock() + self.service1 = demo.DemoNotificationService(self.hass) + self.service2 = demo.DemoNotificationService(self.hass) + + self.service1.send_message = MagicMock(autospec=True) + self.service2.send_message = MagicMock(autospec=True) def mock_get_service(hass, config, discovery_info=None): if config['name'] == 'demo1': @@ -37,11 +41,14 @@ class TestNotifyGroup(unittest.TestCase): }] }) - self.service = group.get_service(self.hass, {'services': [ - {'service': 'demo1'}, - {'service': 'demo2', - 'data': {'target': 'unnamed device', - 'data': {'test': 'message'}}}]}) + self.service = run_coroutine_threadsafe( + group.async_get_service(self.hass, {'services': [ + {'service': 'demo1'}, + {'service': 'demo2', + 'data': {'target': 'unnamed device', + 'data': {'test': 'message'}}}]}), + self.hass.loop + ).result() assert self.service is not None @@ -51,17 +58,19 @@ class TestNotifyGroup(unittest.TestCase): def test_send_message_with_data(self): """Test sending a message with to a notify group.""" - self.service.send_message('Hello', title='Test notification', - data={'hello': 'world'}) + run_coroutine_threadsafe( + self.service.async_send_message( + 'Hello', title='Test notification', data={'hello': 'world'}), + self.hass.loop).result() self.hass.block_till_done() + assert self.service1.send_message.mock_calls[0][1][0] == 'Hello' assert self.service1.send_message.mock_calls[0][2] == { - 'message': 'Hello', 'title': 'Test notification', 'data': {'hello': 'world'} } + assert self.service2.send_message.mock_calls[0][1][0] == 'Hello' assert self.service2.send_message.mock_calls[0][2] == { - 'message': 'Hello', 'target': ['unnamed device'], 'title': 'Test notification', 'data': {'hello': 'world', 'test': 'message'} From eb06023aa58c40f9d3879af4ddeda255701bb620 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Wed, 18 Jan 2017 01:15:37 -0500 Subject: [PATCH 046/191] Fix universal mp service call wth no child (#5411) --- .../components/media_player/universal.py | 4 ++++ .../components/media_player/test_universal.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 45c30b979a6..e01717f5693 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -190,6 +190,10 @@ class UniversalMediaPlayer(MediaPlayerDevice): return active_child = self._child_state + if active_child is None: + # No child to call service on + return + service_data[ATTR_ENTITY_ID] = active_child.entity_id self.hass.services.call(DOMAIN, service_name, service_data, diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index ff70fe36a17..4a06d989ce2 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -538,6 +538,25 @@ class TestMediaPlayer(unittest.TestCase): self.assertEqual(check_flags, ump.supported_media_commands) + def test_service_call_no_active_child(self): + """Test a service call to children with no active child.""" + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.mock_mp_1._state = STATE_OFF + self.mock_mp_1.update_ha_state() + self.mock_mp_2._state = STATE_OFF + self.mock_mp_2.update_ha_state() + ump.update() + + ump.turn_off() + self.assertEqual(0, len(self.mock_mp_1.service_calls['turn_off'])) + self.assertEqual(0, len(self.mock_mp_2.service_calls['turn_off'])) + def test_service_call_to_child(self): """Test service calls that should be routed to a child.""" config = self.config_children_only From 3267aa8c089d3afc98c14428795112c60fdb3ca7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2017 11:01:31 +0100 Subject: [PATCH 047/191] WIP fritz install dependencies fix (#5399) * updated fritzconnection dependency to 0.6 from pypi * updated requirements_all for new dependencies of fritz platform --- homeassistant/components/device_tracker/fritz.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 055c3bc85c0..c262a8fdf2a 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -15,9 +15,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.util import Throttle -REQUIREMENTS = ['https://github.com/deisi/fritzconnection/archive/' - 'b5c14515e1c8e2652b06b6316a7f3913df942841.zip' - '#fritzconnection==0.4.6'] +REQUIREMENTS = ['fritzconnection==0.6'] # Return cached results if last scan was less then this time ago. MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) diff --git a/requirements_all.txt b/requirements_all.txt index b8f93caa5b0..b8fe4898d70 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,6 +139,9 @@ flux_led==0.12 # homeassistant.components.notify.free_mobile freesms==0.1.1 +# homeassistant.components.device_tracker.fritz +# fritzconnection==0.6 + # homeassistant.components.conversation fuzzywuzzy==0.14.0 @@ -210,9 +213,6 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.media_player.onkyo https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.2 -# homeassistant.components.device_tracker.fritz -# https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6 - # homeassistant.components.netatmo https://github.com/jabesq/netatmo-api-python/archive/v0.9.0.zip#lnetatmo==0.9.0 From b7bf07eaca92f5c948a5868578c61b707e8ed946 Mon Sep 17 00:00:00 2001 From: Gianluca Barbaro Date: Wed, 18 Jan 2017 12:20:39 +0100 Subject: [PATCH 048/191] Update generic_thermostat.py After _control_heating() is executed, current_operation() is correctly called but _is_device_active() still reports the old state if the heater switch, at least in my case. The resulting climate state is incorrect until the next refresh, which apparently occurs only when _sensor_changed() gets called (it can be minutes after). I think the state of the heater switch should be forced to update at the end of _control_heating(), but I don't know how to do that... A simple sleep() fixes it, but obviously is just a temporary workaround, I'm not really expecting this PR to be actually committed, unless there's no other solution. --- homeassistant/components/climate/generic_thermostat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index a40795c37c5..a61c71a6551 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -16,6 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv +import time _LOGGER = logging.getLogger(__name__) @@ -222,6 +223,7 @@ class GenericThermostat(ClimateDevice): if too_cold: _LOGGER.info('Turning on heater %s', self.heater_entity_id) switch.turn_on(self.hass, self.heater_entity_id) + time.sleep(.1) @property def _is_device_active(self): From 72dca1da091945651788ed6cc5e314570b79361f Mon Sep 17 00:00:00 2001 From: Gianluca Barbaro Date: Wed, 18 Jan 2017 12:39:16 +0100 Subject: [PATCH 049/191] Update generic_thermostat.py --- homeassistant/components/climate/generic_thermostat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index a61c71a6551..efebcbbf7e9 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.generic_thermostat/ """ import logging +import time import voluptuous as vol @@ -16,7 +17,6 @@ from homeassistant.const import ( from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv -import time _LOGGER = logging.getLogger(__name__) From 5299c923526f08ed711146aa4838a8d895db6550 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 18 Jan 2017 22:52:11 +0100 Subject: [PATCH 050/191] Bugfix volume up/down (#5426) --- homeassistant/components/media_player/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f97b169e1bc..576dca25a6a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -757,6 +757,7 @@ class MediaPlayerDevice(Entity): if hasattr(self, 'volume_up'): # pylint: disable=no-member yield from self.hass.loop.run_in_executor(None, self.volume_up) + return if self.volume_level < 1: yield from self.async_set_volume_level( @@ -771,6 +772,7 @@ class MediaPlayerDevice(Entity): if hasattr(self, 'volume_down'): # pylint: disable=no-member yield from self.hass.loop.run_in_executor(None, self.volume_down) + return if self.volume_level > 0: yield from self.async_set_volume_level( From 216ac14b3d9d1beae46cacc82fcc3ec5532f8dfb Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Wed, 18 Jan 2017 22:15:51 -0500 Subject: [PATCH 051/191] Fix test for async media player volume helpers (#5432) --- tests/components/media_player/test_async_helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 32b527ac4f1..784c54f6d62 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -105,7 +105,8 @@ class TestSyncMediaPlayer(unittest.TestCase): self.assertEqual(self.player.volume_level, 0) self.player.set_volume_level(0.5) self.assertEqual(self.player.volume_level, 0.5) - self.player.volume_up() + run_coroutine_threadsafe( + self.player.async_volume_up(), self.hass.loop).result() self.assertEqual(self.player.volume_level, 0.7) def test_volume_down(self): @@ -113,5 +114,6 @@ class TestSyncMediaPlayer(unittest.TestCase): self.assertEqual(self.player.volume_level, 0) self.player.set_volume_level(0.5) self.assertEqual(self.player.volume_level, 0.5) - self.player.volume_down() + run_coroutine_threadsafe( + self.player.async_volume_down(), self.hass.loop).result() self.assertEqual(self.player.volume_level, 0.3) From a87d653077b0a83c6fef29854694d1f225e50804 Mon Sep 17 00:00:00 2001 From: Gianluca Barbaro Date: Thu, 19 Jan 2017 10:57:45 +0100 Subject: [PATCH 052/191] Update generic_thermostat.py As suggested, I added a callback on the heater switch state change. Still it doesn't solve the problem. --- homeassistant/components/climate/generic_thermostat.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index efebcbbf7e9..3bf64fab2df 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.generic_thermostat/ """ import logging -import time import voluptuous as vol @@ -88,6 +87,7 @@ class GenericThermostat(ClimateDevice): self._unit = hass.config.units.temperature_unit track_state_change(hass, sensor_entity_id, self._sensor_changed) + track_state_change(hass, heater_entity_id, self._switch_changed) sensor_state = hass.states.get(sensor_entity_id) if sensor_state: @@ -166,6 +166,12 @@ class GenericThermostat(ClimateDevice): self._control_heating() self.schedule_update_ha_state() + def _switch_changed(self, entity_id, old_state, new_state): + """Called when heater switch changes state.""" + if new_state is None: + return + self.schedule_update_ha_state() + def _update_temp(self, state): """Update thermostat with latest state from sensor.""" unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -223,7 +229,6 @@ class GenericThermostat(ClimateDevice): if too_cold: _LOGGER.info('Turning on heater %s', self.heater_entity_id) switch.turn_on(self.hass, self.heater_entity_id) - time.sleep(.1) @property def _is_device_active(self): From 909978b0d1a763861154cfc454890ff1030192c6 Mon Sep 17 00:00:00 2001 From: Touliloup Date: Thu, 19 Jan 2017 15:05:37 +0100 Subject: [PATCH 053/191] [Device Tracker] Xiaomi Mi Router token refresh (#5437) Device token is refreshed if not anymore valid (for example after router reboot). Token refresh will only be tried once per update. --- .../components/device_tracker/xiaomi.py | 104 ++++++++++++------ .../components/device_tracker/test_xiaomi.py | 50 ++++++++- 2 files changed, 118 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/device_tracker/xiaomi.py b/homeassistant/components/device_tracker/xiaomi.py index ff53d1fe99f..7c5c415f054 100644 --- a/homeassistant/components/device_tracker/xiaomi.py +++ b/homeassistant/components/device_tracker/xiaomi.py @@ -31,12 +31,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_scanner(hass, config): """Validate the configuration and return a Xiaomi Device Scanner.""" - scanner = XioamiDeviceScanner(config[DOMAIN]) + scanner = XiaomiDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None -class XioamiDeviceScanner(DeviceScanner): +class XiaomiDeviceScanner(DeviceScanner): """This class queries a Xiaomi Mi router. Adapted from Luci scanner. @@ -44,15 +44,14 @@ class XioamiDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - host = config[CONF_HOST] - username, password = config[CONF_USERNAME], config[CONF_PASSWORD] + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] self.lock = threading.Lock() self.last_results = {} - self.token = _get_token(host, username, password) - - self.host = host + self.token = _get_token(self.host, self.username, self.password) self.mac2name = None self.success_init = self.token is not None @@ -66,9 +65,7 @@ class XioamiDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" with self.lock: if self.mac2name is None: - url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist" - url = url.format(self.host, self.token) - result = _get_device_list(url) + result = self._retrieve_list_with_retry() if result: hosts = [x for x in result if 'mac' in x and 'name' in x] @@ -76,7 +73,7 @@ class XioamiDeviceScanner(DeviceScanner): (x['mac'].upper(), x['name']) for x in hosts] self.mac2name = dict(mac2name_list) else: - # Error, handled in the _req_json_rpc + # Error, handled in the _retrieve_list_with_retry return return self.mac2name.get(device.upper(), None) @@ -90,29 +87,72 @@ class XioamiDeviceScanner(DeviceScanner): return False with self.lock: - _LOGGER.info('Refreshing device list') - url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist" - url = url.format(self.host, self.token) - result = _get_device_list(url) + result = self._retrieve_list_with_retry() if result: - self.last_results = [] - for device_entry in result: - # Check if the device is marked as connected - if int(device_entry['online']) == 1: - self.last_results.append(device_entry['mac']) - + self._store_result(result) return True - return False + def _retrieve_list_with_retry(self): + """Retrieve the device list with a retry if token is invalid. -def _get_device_list(url, **kwargs): + Return the list if successful. + """ + _LOGGER.info('Refreshing device list') + result = _retrieve_list(self.host, self.token) + if result: + return result + else: + _LOGGER.info('Refreshing token and retrying device list refresh') + self.token = _get_token(self.host, self.username, self.password) + return _retrieve_list(self.host, self.token) + + def _store_result(self, result): + """Extract and store the device list in self.last_results.""" + self.last_results = [] + for device_entry in result: + # Check if the device is marked as connected + if int(device_entry['online']) == 1: + self.last_results.append(device_entry['mac']) + + +def _retrieve_list(host, token, **kwargs): + """"Get device list for the given host.""" + url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist" + url = url.format(host, token) try: res = requests.get(url, timeout=5, **kwargs) except requests.exceptions.Timeout: - _LOGGER.exception('Connection to the router timed out') + _LOGGER.exception('Connection to the router timed out at URL [%s]', + url) + return + if res.status_code != 200: + _LOGGER.exception('Connection failed with http code [%s]', + res.status_code) + return + try: + result = res.json() + except ValueError: + # If json decoder could not parse the response + _LOGGER.exception('Failed to parse response from mi router') + return + try: + xiaomi_code = result['code'] + except KeyError: + _LOGGER.exception('No field code in response from mi router. %s', + result) + return + if xiaomi_code == 0: + try: + return result['list'] + except KeyError: + _LOGGER.exception('No list in response from mi router. %s', result) + return + else: + _LOGGER.info( + 'Receive wrong Xiaomi code [%s], expected [0] in response [%s]', + xiaomi_code, result) return - return _extract_result(res, 'list') def _get_token(host, username, password): @@ -124,10 +164,6 @@ def _get_token(host, username, password): except requests.exceptions.Timeout: _LOGGER.exception('Connection to the router timed out') return - return _extract_result(res, 'token') - - -def _extract_result(res, key_name): if res.status_code == 200: try: result = res.json() @@ -136,10 +172,12 @@ def _extract_result(res, key_name): _LOGGER.exception('Failed to parse response from mi router') return try: - return result[key_name] + return result['token'] except KeyError: - _LOGGER.exception('No %s in response from mi router. %s', - key_name, result) + error_message = "Xiaomi token cannot be refreshed, response from "\ + + "url: [%s] \nwith parameter: [%s] \nwas: [%s]" + _LOGGER.exception(error_message, url, data, result) return else: - _LOGGER.error('Invalid response from mi router: %s', res) + _LOGGER.error('Invalid response: [%s] at url: [%s] with data [%s]', + res, url, data) diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index 482ed7c0c0d..94a4566a17b 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -15,9 +15,12 @@ from tests.common import get_test_home_assistant _LOGGER = logging.getLogger(__name__) INVALID_USERNAME = 'bob' +TOKEN_TIMEOUT_USERNAME = 'tok' URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login' URL_LIST_END = 'api/misystem/devicelist' +FIRST_CALL = True + def mocked_requests(*args, **kwargs): """Mock requests.get invocations.""" @@ -44,20 +47,38 @@ def mocked_requests(*args, **kwargs): raise requests.HTTPError(self.status_code) data = kwargs.get('data') + global FIRST_CALL if data and data.get('username', None) == INVALID_USERNAME: + # deliver an invalid token return MockResponse({ "code": "401", "msg": "Invalid token" }, 200) + elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME: + # deliver an expired token + return MockResponse({ + "url": "/cgi-bin/luci/;stok=ef5860/web/home", + "token": "timedOut", + "code": "0" + }, 200) elif str(args[0]).startswith(URL_AUTHORIZE): - print("deliver authorized") + # deliver an authorized token return MockResponse({ "url": "/cgi-bin/luci/;stok=ef5860/web/home", "token": "ef5860", "code": "0" }, 200) + elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \ + and FIRST_CALL is True: + FIRST_CALL = False + # deliver an error when called with expired token + return MockResponse({ + "code": "401", + "msg": "Invalid token" + }, 200) elif str(args[0]).endswith(URL_LIST_END): + # deliver the device list return MockResponse({ "mac": "1C:98:EC:0E:D5:A4", "list": [ @@ -144,7 +165,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): self.hass.stop() @mock.patch( - 'homeassistant.components.device_tracker.xiaomi.XioamiDeviceScanner', + 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', return_value=mock.MagicMock()) def test_config(self, xiaomi_mock): """Testing minimal configuration.""" @@ -165,7 +186,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): self.assertEqual(call_arg['platform'], 'device_tracker') @mock.patch( - 'homeassistant.components.device_tracker.xiaomi.XioamiDeviceScanner', + 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner', return_value=mock.MagicMock()) def test_config_full(self, xiaomi_mock): """Testing full configuration.""" @@ -219,3 +240,26 @@ class TestXiaomiDeviceScanner(unittest.TestCase): scanner.get_device_name("23:83:BF:F6:38:A0")) self.assertEqual("Device2", scanner.get_device_name("1D:98:EC:5E:D5:A6")) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_token_timed_out(self, mock_get, mock_post): + """"Testing refresh with a timed out token. + + New token is requested and list is downloaded a second time. + """ + config = { + DOMAIN: xiaomi.PLATFORM_SCHEMA({ + CONF_PLATFORM: xiaomi.DOMAIN, + CONF_HOST: '192.168.0.1', + CONF_USERNAME: TOKEN_TIMEOUT_USERNAME, + CONF_PASSWORD: 'passwordTest' + }) + } + scanner = get_scanner(self.hass, config) + self.assertIsNotNone(scanner) + self.assertEqual(2, len(scanner.scan_devices())) + self.assertEqual("Device1", + scanner.get_device_name("23:83:BF:F6:38:A0")) + self.assertEqual("Device2", + scanner.get_device_name("1D:98:EC:5E:D5:A6")) From 97996317974770a54751f1f025be76bc7633ca46 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 19 Jan 2017 18:53:08 +0100 Subject: [PATCH 054/191] [camera/mjpeg] Support still image for thumbmail (#5440) --- homeassistant/components/camera/mjpeg.py | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index dd030099a45..b29cfcf8949 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -26,12 +26,14 @@ from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_MJPEG_URL = 'mjpeg_url' +CONF_STILL_IMAGE_URL = 'still_image_url' CONTENT_TYPE_HEADER = 'Content-Type' DEFAULT_NAME = 'Mjpeg Camera' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MJPEG_URL): cv.url, + vol.Optional(CONF_STILL_IMAGE_URL): cv.url, vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -70,6 +72,7 @@ class MjpegCamera(Camera): self._username = device_info.get(CONF_USERNAME) self._password = device_info.get(CONF_PASSWORD) self._mjpeg_url = device_info[CONF_MJPEG_URL] + self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._auth = None if self._username and self._password: @@ -78,6 +81,37 @@ class MjpegCamera(Camera): self._username, password=self._password ) + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + # DigestAuth is not supported + if self._authentication == HTTP_DIGEST_AUTHENTICATION or \ + self._still_image_url is None: + image = yield from self.hass.loop.run_in_executor( + None, self.camera_image) + return image + + websession = async_get_clientsession(self.hass) + response = None + try: + with async_timeout.timeout(10, loop=self.hass.loop): + response = websession.get( + self._still_image_url, auth=self._auth) + + image = yield from response.read() + return image + + except asyncio.TimeoutError: + _LOGGER.error('Timeout getting camera image') + + except (aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: + _LOGGER.error('Error getting new camera image: %s', err) + + finally: + if response is not None: + yield from response.release() + def camera_image(self): """Return a still image response from the camera.""" if self._username and self._password: From 8da398c0bd937d6bf83f052b5c920771f33b01cf Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 19 Jan 2017 18:55:27 +0100 Subject: [PATCH 055/191] Proxy aiohttp websession / more rebust. (#5419) --- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/camera/amcrest.py | 43 +++---------------- homeassistant/components/camera/mjpeg.py | 37 ++-------------- homeassistant/components/camera/synology.py | 39 +++-------------- homeassistant/helpers/aiohttp_client.py | 47 +++++++++++++++++++-- 5 files changed, 60 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 89a2f6c5e46..174d0f5a298 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -174,7 +174,7 @@ class Camera(Entity): yield from asyncio.sleep(.5) - except asyncio.CancelledError: + except (asyncio.CancelledError, ConnectionResetError): _LOGGER.debug("Close stream by frontend.") response = None diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index bec760dbe10..604bf519042 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -8,9 +8,6 @@ import asyncio import logging import aiohttp -from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout -import async_timeout import voluptuous as vol import homeassistant.loader as loader @@ -18,7 +15,8 @@ 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 -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_get_clientsession, async_aiohttp_proxy_stream) REQUIREMENTS = ['amcrest==1.1.0'] @@ -108,7 +106,6 @@ class AmcrestCam(Camera): device_info.get(CONF_USERNAME), password=device_info.get(CONF_PASSWORD) ) - self._websession = async_create_clientsession(hass) def camera_image(self): """Return a still image reponse from the camera.""" @@ -125,44 +122,16 @@ class AmcrestCam(Camera): return # Otherwise, stream an MJPEG image stream directly from the camera + websession = async_get_clientsession(self.hass) streaming_url = '%s/mjpg/video.cgi?channel=0&subtype=%d' % ( self._base_url, self._resolution ) - stream = None - response = None - try: - with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - stream = yield from self._websession.get( - streaming_url, - auth=self._token, - timeout=TIMEOUT - ) - response = web.StreamResponse() - response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + stream_coro = websession.get( + streaming_url, auth=self._token, timeout=TIMEOUT) - yield from response.prepare(request) - - while True: - data = yield from stream.content.read(16384) - if not data: - break - response.write(data) - - except (asyncio.TimeoutError, aiohttp.errors.ClientError): - _LOGGER.exception("Error on %s", streaming_url) - raise HTTPGatewayTimeout() - - except asyncio.CancelledError: - _LOGGER.debug("Close stream by frontend.") - response = None - - finally: - if stream is not None: - stream.close() - if response is not None: - yield from response.write_eof() + yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index b29cfcf8949..125daba16c4 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -9,9 +9,6 @@ import logging from contextlib import closing import aiohttp -from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout -import async_timeout import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol @@ -20,7 +17,8 @@ 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.aiohttp_client import ( + async_get_clientsession, async_aiohttp_proxy_stream) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -137,36 +135,9 @@ 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) + stream_coro = websession.get(self._mjpeg_url, auth=self._auth) - response = web.StreamResponse() - response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - - yield from response.prepare(request) - - while True: - data = yield from stream.content.read(102400) - if not data: - break - response.write(data) - - except asyncio.TimeoutError: - raise HTTPGatewayTimeout() - - except asyncio.CancelledError: - _LOGGER.debug("Close stream by frontend.") - response = None - - finally: - if stream is not None: - stream.close() - if response is not None: - yield from response.write_eof() + yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 424e269c555..39939c73d0d 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -10,8 +10,6 @@ import logging import voluptuous as vol import aiohttp -from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout import async_timeout from homeassistant.const import ( @@ -20,7 +18,8 @@ from homeassistant.const import ( from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_create_clientsession) + async_get_clientsession, async_create_clientsession, + async_aiohttp_proxy_stream) import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe @@ -253,38 +252,10 @@ 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 - ) - response = web.StreamResponse() - response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + stream_coro = self._websession.get( + streaming_url, params=streaming_payload) - yield from response.prepare(request) - - 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() - - except asyncio.CancelledError: - _LOGGER.debug("Close stream by frontend.") - response = None - - finally: - if stream is not None: - stream.close() - if response is not None: - yield from response.write_eof() + yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 32e0861ff53..6f74493c078 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,9 +1,13 @@ """Helper for aiohttp webclient stuff.""" -import sys import asyncio -import aiohttp +import sys + +import aiohttp +from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE +from aiohttp import web +from aiohttp.web_exceptions import HTTPGatewayTimeout +import async_timeout -from aiohttp.hdrs import USER_AGENT from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import __version__ @@ -65,6 +69,43 @@ def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, return clientsession +@asyncio.coroutine +def async_aiohttp_proxy_stream(hass, request, stream_coro, buffer_size=102400, + timeout=10): + """Stream websession request to aiohttp web response.""" + response = None + stream = None + + try: + with async_timeout.timeout(timeout, loop=hass.loop): + stream = yield from stream_coro + + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE) + + yield from response.prepare(request) + + while True: + data = yield from stream.content.read(buffer_size) + response.write(data) + + except asyncio.TimeoutError: + raise HTTPGatewayTimeout() + + except (aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError): + pass + + except (asyncio.CancelledError, ConnectionResetError): + response = None + + finally: + if stream is not None: + stream.close() + if response is not None: + yield from response.write_eof() + + @callback # pylint: disable=invalid-name def _async_register_clientsession_shutdown(hass, clientsession): From 11083cf04b491f6db9a12ad9d3bdce3645f47965 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 19 Jan 2017 19:18:32 +0100 Subject: [PATCH 056/191] Fix lint (#5443) --- homeassistant/components/camera/mjpeg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 125daba16c4..fa008cd6534 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -9,6 +9,7 @@ import logging from contextlib import closing import aiohttp +import async_timeout import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol From 3da25c227f89301f569fdb9b7986d4c10a6ff32f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 19 Jan 2017 19:33:15 +0100 Subject: [PATCH 057/191] lint v2 (#5444) --- homeassistant/components/camera/mjpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index fa008cd6534..6501439e24f 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -94,7 +94,7 @@ class MjpegCamera(Camera): response = None try: with async_timeout.timeout(10, loop=self.hass.loop): - response = websession.get( + response = yield from websession.get( self._still_image_url, auth=self._auth) image = yield from response.read() From 6ef9714dc115b8ad6e1e5a205a25d3c3348123eb Mon Sep 17 00:00:00 2001 From: Gianluca Barbaro Date: Thu, 19 Jan 2017 19:46:58 +0100 Subject: [PATCH 058/191] Update generic_thermostat.py --- homeassistant/components/climate/generic_thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 3bf64fab2df..4bf704327d2 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -134,8 +134,8 @@ class GenericThermostat(ClimateDevice): if temperature is None: return self._target_temp = temperature + self.schedule_update_ha_state() self._control_heating() - self.update_ha_state() @property def min_temp(self): @@ -163,8 +163,8 @@ class GenericThermostat(ClimateDevice): return self._update_temp(new_state) - self._control_heating() self.schedule_update_ha_state() + self._control_heating() def _switch_changed(self, entity_id, old_state, new_state): """Called when heater switch changes state.""" From 1a82adb054189fd40314df970da1c6be155545d6 Mon Sep 17 00:00:00 2001 From: David McNett Date: Thu, 19 Jan 2017 13:07:01 -0600 Subject: [PATCH 059/191] New platform media_player/anthemav (#5146) * Initial commit of anthemav platform. It loads but has no purpose. * Now presents a card in the UI but the values aren't real * Mute and volume polling/setting work now * Source lists and selection works now. * Reduce debug logging verbosity * Support power on/off and skip polling for details if power is off * Add some static tables to decode numerics from telnet commands * Add stub for unsupported media_play * New style anthemav uses native asyncio structure * Add device callback for asyncio * This is ugly but it works * Simplify async setup and abstract class data retrieval * Implement commands (power on and power off for now) * Add support for scan_interval and set default to 120 seconds * Pass-through to package handlers for volume and input selection * Slight restructuring to satisfy anthemav 0.9 * Load anthemav package from pypi now that it's registered * Proper app_name from a/v info * Mispelled word * media_player/anthemav initial commit of platform requirements * Philio 3-in-1 Gen 4 zwave sensor needs the no-off-event workaround. (#5120) * Add print_config_parameter service to Z-Wave (#5121) * Add print_config_param service to z-wave * Add print_config_parameter service to z-wave * Add print_config_parameter service to z-wave * Fix typos * Fix typos * Fix typo * Conform to Python/project style requirements * Making pylint happy * Bring pip requirements in agreement with the code * Bungled previous update * Remove unnecessady SCAN_INTERVAL logic I was unawre that this is performed as part of the normal platform behavior and it's unnecessary for a platform to independently implement this logic. * Refactor code based on @armills PR requests * Re-add media_play stub to avoid traceback * Align with platform reqirements * Remove references to SCAN_INTERVAL and clean up _lookup logic * Add DEFAULT_PORT assignment * Code style changes and removal of vestigial structures * CONF_NAME handling changes to allow local override to default from device * Address PR feedback from @balloob * Remove media_play function override It's no longer necesary for the platform to implement a stub media_play function override now that the Add SUPPORT_PLAY flag #5181 issue has been resolved and merged into the dev branch. * Rename callback function to async_ for clarity * Use async routines for platform methods * Convert update callback to coroutine for conformity Underlying anthemav library now properly supports coroutine callbacks instead of normal functions. Converted the platform callback to a coroutine for conformance with async operation for the device. Special thanks to @pvizeli and @armills for their invaluable remedial Python instruction! * Further callback refinements Altered the nature of callback handling based on suggestions from @pvizeli * True not needed for local push update_ha_state * Small style fix --- .coveragerc | 1 + .../components/media_player/anthemav.py | 175 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 179 insertions(+) create mode 100644 homeassistant/components/media_player/anthemav.py diff --git a/.coveragerc b/.coveragerc index 8f67671a5a2..a33538b7e63 100644 --- a/.coveragerc +++ b/.coveragerc @@ -201,6 +201,7 @@ omit = homeassistant/components/light/yeelight.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py + homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py new file mode 100644 index 00000000000..2707a62f7bf --- /dev/null +++ b/homeassistant/components/media_player/anthemav.py @@ -0,0 +1,175 @@ +""" +Support for Anthem Network Receivers and Processors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.anthemav/ +""" +import logging +import asyncio + +import voluptuous as vol + +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, STATE_UNKNOWN, + EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['anthemav==1.1.7'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'anthemav' + +DEFAULT_PORT = 14999 + +SUPPORT_ANTHEMAV = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up our socket to the AVR.""" + import anthemav + + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + device = None + + _LOGGER.info('Provisioning Anthem AVR device at %s:%d', host, port) + + def async_anthemav_update_callback(message): + """Receive notification from transport that new data exists.""" + _LOGGER.info('Received update calback from AVR: %s', message) + hass.async_add_job(device.async_update_ha_state()) + + avr = yield from anthemav.Connection.create( + host=host, port=port, loop=hass.loop, + update_callback=async_anthemav_update_callback) + + device = AnthemAVR(avr, name) + + _LOGGER.debug('dump_devicedata: '+device.dump_avrdata) + _LOGGER.debug('dump_conndata: '+avr.dump_conndata) + _LOGGER.debug('dump_rawdata: '+avr.protocol.dump_rawdata) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) + yield from async_add_devices([device]) + + +class AnthemAVR(MediaPlayerDevice): + """Entity reading values from Anthem AVR protocol.""" + + def __init__(self, avr, name): + """"Initialize entity with transport.""" + super().__init__() + self.avr = avr + self._name = name + + def _lookup(self, propname, dval=None): + return getattr(self.avr.protocol, propname, dval) + + @property + def supported_media_commands(self): + """Return flag of media commands that are supported.""" + return SUPPORT_ANTHEMAV + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return name of device.""" + return self._name or self._lookup('model') + + @property + def state(self): + """Return state of power on/off.""" + pwrstate = self._lookup('power') + + if pwrstate is True: + return STATE_ON + elif pwrstate is False: + return STATE_OFF + else: + return STATE_UNKNOWN + + @property + def is_volume_muted(self): + """Return boolean reflecting mute state on device.""" + return self._lookup('mute', False) + + @property + def volume_level(self): + """Return volume level from 0 to 1.""" + return self._lookup('volume_as_percentage', 0.0) + + @property + def media_title(self): + """Return current input name (closest we have to media title).""" + return self._lookup('input_name', 'No Source') + + @property + def app_name(self): + """Return details about current video and audio stream.""" + return self._lookup('video_input_resolution_text', '') + ' ' \ + + self._lookup('audio_input_name', '') + + @property + def source(self): + """Return currently selected input.""" + return self._lookup('input_name', "Unknown") + + @property + def source_list(self): + """Return all active, configured inputs.""" + return self._lookup('input_list', ["Unknown"]) + + @asyncio.coroutine + def async_select_source(self, source): + """Change AVR to the designated source (by name).""" + self._update_avr('input_name', source) + + @asyncio.coroutine + def async_turn_off(self): + """Turn AVR power off.""" + self._update_avr('power', False) + + @asyncio.coroutine + def async_turn_on(self): + """Turn AVR power on.""" + self._update_avr('power', True) + + @asyncio.coroutine + def async_set_volume_level(self, volume): + """Set AVR volume (0 to 1).""" + self._update_avr('volume_as_percentage', volume) + + @asyncio.coroutine + def async_mute_volume(self, mute): + """Engage AVR mute.""" + self._update_avr('mute', mute) + + def _update_avr(self, propname, value): + """Update a property in the AVR.""" + _LOGGER.info('Sending command to AVR: set '+propname+' to '+str(value)) + setattr(self.avr.protocol, propname, value) + + @property + def dump_avrdata(self): + """Return state of avr object for debugging forensics.""" + attrs = vars(self) + return( + 'dump_avrdata: ' + + ', '.join('%s: %s' % item for item in attrs.items())) diff --git a/requirements_all.txt b/requirements_all.txt index b8fe4898d70..58339edbbaa 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,6 +40,9 @@ aiohttp_cors==0.5.0 # homeassistant.components.sensor.amcrest amcrest==1.1.0 +# homeassistant.components.media_player.anthemav +anthemav==1.1.7 + # homeassistant.components.apcupsd apcaccess==0.0.4 From 3b25b5a6da19cc5a926154496dc7d726c0f4d4a8 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Thu, 19 Jan 2017 12:07:37 -0800 Subject: [PATCH 060/191] Add support for Avion Bluetooth dimmer switches (#5414) GE sell a range of Bluetooth dimmer switches based on Avi-on technology. Add a module for controlling them. There's also a set of smart switches that speak the same protocol, but I don't have any of those to test support with. --- .coveragerc | 1 + homeassistant/components/light/avion.py | 117 ++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 121 insertions(+) create mode 100644 homeassistant/components/light/avion.py diff --git a/.coveragerc b/.coveragerc index a33538b7e63..0914e2edde1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -189,6 +189,7 @@ omit = homeassistant/components/joaoapps_join.py homeassistant/components/keyboard.py homeassistant/components/keyboard_remote.py + homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/flux_led.py homeassistant/components/light/hue.py diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py new file mode 100644 index 00000000000..7dded545620 --- /dev/null +++ b/homeassistant/components/light/avion.py @@ -0,0 +1,117 @@ +""" +Support for Avion dimmers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.avion/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, + PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['avion==0.5'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_AVION_LED = (SUPPORT_BRIGHTNESS) + +DEVICE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up an Avion switch.""" + lights = [] + for address, device_config in config[CONF_DEVICES].items(): + device = {} + device['name'] = device_config[CONF_NAME] + device['key'] = device_config[CONF_API_KEY] + device['address'] = address + light = AvionLight(device) + if light.is_valid: + lights.append(light) + + add_devices(lights) + + +class AvionLight(Light): + """Representation of an Avion light.""" + + def __init__(self, device): + """Initialize the light.""" + import avion + + self._name = device['name'] + self._address = device['address'] + self._key = device["key"] + self._brightness = 255 + self._state = False + self._switch = avion.avion(self._address, self._key) + self._switch.connect() + self.is_valid = True + + @property + def unique_id(self): + """Return the ID of this light.""" + return "{}.{}".format(self.__class__, self._address) + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_AVION_LED + + @property + def should_poll(self): + """Don't poll.""" + return False + + @property + def assumed_state(self): + """We can't read the actual state, so assume it matches.""" + return True + + def set_state(self, brightness): + """Set the state of this lamp to the provided brightness.""" + self._switch.set_brightness(brightness) + return True + + def turn_on(self, **kwargs): + """Turn the specified or all lights on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + + if brightness is not None: + self._brightness = brightness + + self.set_state(self.brightness) + self._state = True + + def turn_off(self, **kwargs): + """Turn the specified or all lights off.""" + self.set_state(0) + self._state = False diff --git a/requirements_all.txt b/requirements_all.txt index 58339edbbaa..1c806033e2f 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,6 +52,9 @@ apns2==0.1.1 # homeassistant.components.sun astral==1.3.3 +# homeassistant.components.light.avion +avion==0.5 + # homeassistant.components.sensor.linux_battery batinfo==0.4.2 From 64c9cd805ab4e9b47e5f2cfe67cac1f33e1f3acd Mon Sep 17 00:00:00 2001 From: Gianluca Barbaro Date: Thu, 19 Jan 2017 21:26:12 +0100 Subject: [PATCH 061/191] Update generic_thermostat.py --- homeassistant/components/climate/generic_thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 4bf704327d2..562847567a3 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -134,8 +134,8 @@ class GenericThermostat(ClimateDevice): if temperature is None: return self._target_temp = temperature - self.schedule_update_ha_state() self._control_heating() + self.schedule_update_ha_state() @property def min_temp(self): @@ -163,8 +163,8 @@ class GenericThermostat(ClimateDevice): return self._update_temp(new_state) - self.schedule_update_ha_state() self._control_heating() + self.schedule_update_ha_state() def _switch_changed(self, entity_id, old_state, new_state): """Called when heater switch changes state.""" From 62b785c0408570de78cfda27012fc21840ef9014 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 19 Jan 2017 22:08:55 +0100 Subject: [PATCH 062/191] update rfxtrx lib --- homeassistant/components/rfxtrx.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 56026168383..6918a596988 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS) -REQUIREMENTS = ['pyRFXtrx==0.14.0'] +REQUIREMENTS = ['pyRFXtrx==0.15.0'] DOMAIN = "rfxtrx" @@ -34,6 +34,7 @@ EVENT_BUTTON_PRESSED = 'button_pressed' DATA_TYPES = OrderedDict([ ('Temperature', TEMP_CELSIUS), + ('Temperature2', TEMP_CELSIUS), ('Humidity', '%'), ('Barometer', ''), ('Wind direction', ''), diff --git a/requirements_all.txt b/requirements_all.txt index 1c806033e2f..eb244ac6080 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ py-cpuinfo==0.2.3 pyHS100==0.2.3 # homeassistant.components.rfxtrx -pyRFXtrx==0.14.0 +pyRFXtrx==0.15.0 # homeassistant.components.notify.xmpp pyasn1-modules==0.0.8 From a74258db09e8d70a07127a1be443f2af1b396a3d Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 19 Jan 2017 13:14:48 -0800 Subject: [PATCH 063/191] Block Avion from auto installing because only supported on Linux (cc #5414) --- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 1c806033e2f..b895ff5cdb7 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -53,7 +53,7 @@ apns2==0.1.1 astral==1.3.3 # homeassistant.components.light.avion -avion==0.5 +# avion==0.5 # homeassistant.components.sensor.linux_battery batinfo==0.4.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 81fb17aac17..e23c8d09fea 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -20,6 +20,7 @@ COMMENT_REQUIREMENTS = ( 'evdev', 'pycups', 'python-eq3bt', + 'avion' ) IGNORE_PACKAGES = ( From 738292f8176141250249e9775be0b049fe6db13a Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 19 Jan 2017 13:25:35 -0800 Subject: [PATCH 064/191] Ignore import error on avion --- homeassistant/components/light/avion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index 7dded545620..4a53697ccf4 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -50,6 +50,7 @@ class AvionLight(Light): def __init__(self, device): """Initialize the light.""" + # pylint: disable=import-error import avion self._name = device['name'] From dbcad34b47e4adadf02c6f76d076396074f73dde Mon Sep 17 00:00:00 2001 From: HerrHofrat Date: Thu, 19 Jan 2017 23:33:31 +0100 Subject: [PATCH 065/191] Updated valid station id list (#5449) --- homeassistant/components/sensor/zamg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py index 6bb9dd0748d..6f621b683b6 100644 --- a/homeassistant/components/sensor/zamg.py +++ b/homeassistant/components/sensor/zamg.py @@ -35,7 +35,8 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) VALID_STATION_IDS = ( '11010', '11012', '11022', '11035', '11036', '11101', '11121', '11126', - '11130', '11150', '11155', '11157', '11171', '11190', '11204' + '11130', '11150', '11155', '11157', '11171', '11190', '11204', '11240', + '11244', '11265', '11331', '11343', '11389' ) SENSOR_TYPES = { From 887a33c7d1c1fad01f7cfc22149b488ad35f3006 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Jan 2017 21:27:10 -0800 Subject: [PATCH 066/191] Persist emulated hue IDs (#5435) --- .../components/emulated_hue/__init__.py | 40 +++++++++++++++++-- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/emulated_hue/test_init.py | 37 +++++++++++------ 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index d412a7af91f..d95224c9469 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/emulated_hue/ """ import asyncio +import json import logging import voluptuous as vol @@ -24,6 +25,8 @@ DOMAIN = 'emulated_hue' _LOGGER = logging.getLogger(__name__) +NUMBERS_FILE = 'emulated_hue_ids.json' + CONF_HOST_IP = 'host_ip' CONF_LISTEN_PORT = 'listen_port' CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' @@ -63,7 +66,7 @@ ATTR_EMULATED_HUE = 'emulated_hue' def setup(hass, yaml_config): """Activate the emulated_hue component.""" - config = Config(yaml_config.get(DOMAIN, {})) + config = Config(hass, yaml_config.get(DOMAIN, {})) server = HomeAssistantWSGI( hass, @@ -112,10 +115,11 @@ def setup(hass, yaml_config): class Config(object): """Holds configuration variables for the emulated hue bridge.""" - def __init__(self, conf): + def __init__(self, hass, conf): """Initialize the instance.""" + self.hass = hass self.type = conf.get(CONF_TYPE) - self.numbers = {} + self.numbers = None self.cached_states = {} # Get the IP address that will be passed to the Echo during discovery @@ -165,6 +169,9 @@ class Config(object): if self.type == TYPE_ALEXA: return entity_id + if self.numbers is None: + self.numbers = self._load_numbers_json() + # Google Home for number, ent_id in self.numbers.items(): if entity_id == ent_id: @@ -172,6 +179,7 @@ class Config(object): number = str(len(self.numbers) + 1) self.numbers[number] = entity_id + self._save_numbers_json() return number def number_to_entity_id(self, number): @@ -179,6 +187,9 @@ class Config(object): if self.type == TYPE_ALEXA: return number + if self.numbers is None: + self.numbers = self._load_numbers_json() + # Google Home assert isinstance(number, str) return self.numbers.get(number) @@ -205,3 +216,26 @@ class Config(object): domain_exposed_by_default and explicit_expose is not False return is_default_exposed or explicit_expose + + def _load_numbers_json(self): + """Helper method to load numbers json.""" + try: + with open(self.hass.config.path(NUMBERS_FILE), + encoding='utf-8') as fil: + return json.loads(fil.read()) + except (OSError, ValueError) as err: + # OSError if file not found or unaccessible/no permissions + # ValueError if could not parse JSON + if not isinstance(err, FileNotFoundError): + _LOGGER.warning('Failed to open %s: %s', NUMBERS_FILE, err) + return {} + + def _save_numbers_json(self): + """Helper method to save numbers json.""" + try: + with open(self.hass.config.path(NUMBERS_FILE), 'w', + encoding='utf-8') as fil: + fil.write(json.dumps(self.numbers)) + except OSError as err: + # OSError if file write permissions + _LOGGER.warning('Failed to write %s: %s', NUMBERS_FILE, err) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 0b36b835cd5..7c73e933fd3 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -106,7 +106,7 @@ def hass_hue(loop, hass): def hue_client(loop, hass_hue, test_client): """Create web client for emulated hue api.""" web_app = mock_http_component_app(hass_hue) - config = Config({'type': 'alexa'}) + config = Config(None, {'type': 'alexa'}) HueUsernameView().register(web_app.router) HueAllLightsStateView(config).register(web_app.router) diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 2ee7c385d8d..8c0a6dc4f60 100755 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,31 +1,44 @@ """Test the Emulated Hue component.""" -from unittest.mock import patch +import json + +from unittest.mock import patch, Mock, mock_open from homeassistant.components.emulated_hue import Config, _LOGGER def test_config_google_home_entity_id_to_number(): """Test config adheres to the type.""" - conf = Config({ + conf = Config(Mock(), { 'type': 'google_home' }) - number = conf.entity_id_to_number('light.test') - assert number == '1' + mop = mock_open(read_data=json.dumps({'1': 'light.test2'})) + handle = mop() - number = conf.entity_id_to_number('light.test') - assert number == '1' + with patch('homeassistant.components.emulated_hue.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } - number = conf.entity_id_to_number('light.test2') - assert number == '2' + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test' + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 + + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): """Test config adheres to the type.""" - conf = Config({ + conf = Config(None, { 'type': 'alexa' }) @@ -45,7 +58,7 @@ def test_config_alexa_entity_id_to_number(): def test_warning_config_google_home_listen_port(): """Test we warn when non-default port is used for Google Home.""" with patch.object(_LOGGER, 'warning') as mock_warn: - Config({ + Config(None, { 'type': 'google_home', 'host_ip': '123.123.123.123', 'listen_port': 8300 From f17efc216876433ed324aaaa7bbe4a28ce5cb5e4 Mon Sep 17 00:00:00 2001 From: happyleavesaoc Date: Fri, 20 Jan 2017 00:31:44 -0500 Subject: [PATCH 067/191] log formats match (#5456) --- homeassistant/bootstrap.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index da7886ad1e8..0c8b0bc688e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -508,8 +508,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, Async friendly. """ logging.basicConfig(level=logging.INFO) - fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " - "[%(name)s] %(message)s%(reset)s") + fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " + "[%(name)s] %(message)s") + colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + datefmt = '%y-%m-%d %H:%M:%S' # suppress overly verbose logs from libraries that aren't helpful logging.getLogger("requests").setLevel(logging.WARNING) @@ -519,8 +521,8 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, try: from colorlog import ColoredFormatter logging.getLogger().handlers[0].setFormatter(ColoredFormatter( - fmt, - datefmt='%y-%m-%d %H:%M:%S', + colorfmt, + datefmt=datefmt, reset=True, log_colors={ 'DEBUG': 'cyan', @@ -554,9 +556,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, err_log_path, mode='w', delay=True) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) - err_handler.setFormatter( - logging.Formatter('%(asctime)s %(name)s: %(message)s', - datefmt='%y-%m-%d %H:%M:%S')) + err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) async_handler = AsyncHandler(hass.loop, err_handler) hass.data[core.DATA_ASYNCHANDLER] = async_handler From fe6a8f3367e520da7921911179f87c880bb0c92c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Jan 2017 07:21:25 +0100 Subject: [PATCH 068/191] Remove old openalpr component (#5406) * Remove old openalpr component * update region support --- .coveragerc | 1 - .../image_processing/openalpr_cloud.py | 8 +- .../image_processing/openalpr_local.py | 9 +- homeassistant/components/openalpr.py | 472 ------------------ requirements_all.txt | 6 - 5 files changed, 13 insertions(+), 483 deletions(-) delete mode 100644 homeassistant/components/openalpr.py diff --git a/.coveragerc b/.coveragerc index 0914e2edde1..5bd097e8abb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -266,7 +266,6 @@ omit = homeassistant/components/notify/twitter.py 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/amcrest.py diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 61b3442856a..d17291df07f 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -26,14 +26,18 @@ _LOGGER = logging.getLogger(__name__) OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize" OPENALPR_REGIONS = [ - 'us', - 'eu', 'au', 'auwide', + 'br', + 'eu', + 'fr', 'gb', 'kr', + 'kr2', 'mx', 'sg', + 'us', + 'vn2' ] CONF_REGION = 'region' diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index a1736c00ffc..65c2a683341 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -31,16 +31,21 @@ ATTR_PLATES = 'plates' ATTR_VEHICLES = 'vehicles' OPENALPR_REGIONS = [ - 'us', - 'eu', 'au', 'auwide', + 'br', + 'eu', + 'fr', 'gb', 'kr', + 'kr2', 'mx', 'sg', + 'us', + 'vn2' ] + CONF_REGION = 'region' CONF_ALPR_BIN = 'alp_bin' diff --git a/homeassistant/components/openalpr.py b/homeassistant/components/openalpr.py deleted file mode 100644 index eaaba5f8af8..00000000000 --- a/homeassistant/components/openalpr.py +++ /dev/null @@ -1,472 +0,0 @@ -""" -Component that will help set the openalpr for video streams. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/openalpr/ -""" -from base64 import b64encode -import logging -import os -from time import time - -import requests -import voluptuous as vol - -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, - EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) -from homeassistant.components.ffmpeg import ( - get_binary, run_test, CONF_INPUT, CONF_EXTRA_ARGUMENTS) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent - -DOMAIN = 'openalpr' -DEPENDENCIES = ['ffmpeg'] -REQUIREMENTS = [ - 'https://github.com/pvizeli/cloudapi/releases/download/1.0.2/' - 'python-1.0.2.zip#openalpr_api==1.0.2', - 'ha-alpr==0.3'] - -_LOGGER = logging.getLogger(__name__) - -SERVICE_SCAN = 'scan' -SERVICE_RESTART = 'restart' - -EVENT_FOUND = 'openalpr.found' - -ATTR_PLATE = 'plate' - - -ENGINE_LOCAL = 'local' -ENGINE_CLOUD = 'cloud' - -RENDER_IMAGE = 'image' -RENDER_FFMPEG = 'ffmpeg' - -OPENALPR_REGIONS = [ - 'us', - 'eu', - 'au', - 'auwide', - 'gb', - 'kr', - 'mx', - 'sg', -] - -CONF_RENDER = 'render' -CONF_ENGINE = 'engine' -CONF_REGION = 'region' -CONF_INTERVAL = 'interval' -CONF_ENTITIES = 'entities' -CONF_CONFIDENCE = 'confidence' -CONF_ALPR_BINARY = 'alpr_binary' - -DEFAULT_NAME = 'OpenAlpr' -DEFAULT_ENGINE = ENGINE_LOCAL -DEFAULT_RENDER = RENDER_FFMPEG -DEFAULT_BINARY = 'alpr' -DEFAULT_INTERVAL = 10 -DEFAULT_CONFIDENCE = 80.0 - -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_RENDER, default=DEFAULT_RENDER): - vol.In([RENDER_IMAGE, RENDER_FFMPEG]), - vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ENGINE): vol.In([ENGINE_LOCAL, ENGINE_CLOUD]), - vol.Required(CONF_REGION): vol.In(OPENALPR_REGIONS), - vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE): - vol.Coerce(float), - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ALPR_BINARY, default=DEFAULT_BINARY): cv.string, - vol.Required(CONF_ENTITIES): - vol.All(cv.ensure_list, [DEVICE_SCHEMA]), - }) -}, extra=vol.ALLOW_EXTRA) - - -SERVICE_RESTART_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -SERVICE_SCAN_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - - -def scan(hass, entity_id=None): - """Scan a image immediately.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_SCAN, data) - - -def restart(hass, entity_id=None): - """Restart a ffmpeg process.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_RESTART, data) - - -def setup(hass, config): - """Setup the OpenAlpr component.""" - engine = config[DOMAIN].get(CONF_ENGINE) - region = config[DOMAIN].get(CONF_REGION) - confidence = config[DOMAIN].get(CONF_CONFIDENCE) - api_key = config[DOMAIN].get(CONF_API_KEY) - binary = config[DOMAIN].get(CONF_ALPR_BINARY) - use_render_fffmpeg = False - - _LOGGER.warning("This platform is replaced by 'image_processing' and will " - "be removed in a future version!") - - component = EntityComponent(_LOGGER, DOMAIN, hass) - openalpr_device = [] - - for device in config[DOMAIN].get(CONF_ENTITIES): - input_source = device.get(CONF_INPUT) - render = device.get(CONF_RENDER) - - ## - # create api - if engine == ENGINE_LOCAL: - alpr_api = OpenalprApiLocal( - confidence=confidence, - region=region, - binary=binary, - ) - else: - alpr_api = OpenalprApiCloud( - confidence=confidence, - region=region, - api_key=api_key, - ) - - ## - # Create Alpr device / render engine - if render == RENDER_FFMPEG: - use_render_fffmpeg = True - if not run_test(hass, input_source): - _LOGGER.error("'%s' is not valid ffmpeg input", input_source) - continue - - alpr_dev = OpenalprDeviceFFmpeg( - name=device.get(CONF_NAME), - interval=device.get(CONF_INTERVAL), - api=alpr_api, - input_source=input_source, - extra_arguments=device.get(CONF_EXTRA_ARGUMENTS), - ) - else: - alpr_dev = OpenalprDeviceImage( - name=device.get(CONF_NAME), - interval=device.get(CONF_INTERVAL), - api=alpr_api, - input_source=input_source, - username=device.get(CONF_USERNAME), - password=device.get(CONF_PASSWORD), - ) - - # register shutdown event - openalpr_device.append(alpr_dev) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, alpr_dev.shutdown) - - component.add_entities(openalpr_device) - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - def _handle_service_scan(service): - """Handle service for immediately scan.""" - device_list = component.extract_from_service(service) - - for device in device_list: - device.scan() - - hass.services.register(DOMAIN, SERVICE_SCAN, - _handle_service_scan, - descriptions[DOMAIN][SERVICE_SCAN], - schema=SERVICE_SCAN_SCHEMA) - - # Add restart service only if a device use ffmpeg as render - if not use_render_fffmpeg: - return True - - def _handle_service_restart(service): - """Handle service for restart ffmpeg process.""" - device_list = component.extract_from_service(service) - - for device in device_list: - device.restart() - - hass.services.register(DOMAIN, SERVICE_RESTART, - _handle_service_restart, - descriptions[DOMAIN][SERVICE_RESTART], - schema=SERVICE_RESTART_SCHEMA) - - return True - - -class OpenalprDevice(Entity): - """Represent a openalpr device object for processing stream/images.""" - - def __init__(self, name, interval, api): - """Init image processing.""" - self._name = name - self._interval = interval - self._api = api - self._last = {} - - @property - def state(self): - """Return the state of the entity.""" - confidence = 0 - plate = STATE_UNKNOWN - - # search high plate - for i_pl, i_co in self._last.items(): - if i_co > confidence: - confidence = i_co - plate = i_pl - return plate - - def shutdown(self, event): - """Close stream.""" - if hasattr(self._api, "shutdown"): - self._api.shutdown(event) - - def restart(self): - """Restart stream.""" - raise NotImplementedError() - - def _process_image(self, image): - """Callback for processing image.""" - self._api.process_image(image, self._process_event) - - def _process_event(self, plates): - """Send event with new plates.""" - state_change = False - plates_set = set(plates) - last_set = set(self._last) - new_plates = plates_set - last_set - - # send events - for i_plate in new_plates: - self.hass.bus.fire(EVENT_FOUND, { - ATTR_PLATE: i_plate, - ATTR_ENTITY_ID: self.entity_id - }) - - # update entity store - if last_set <= plates_set: - state_change = True - self._last = plates - - # update HA state - if state_change: - self.update_ha_state() - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - return {'plates': self._last} - - def scan(self): - """Immediately scan a image.""" - raise NotImplementedError() - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - -class OpenalprDeviceFFmpeg(OpenalprDevice): - """Represent a openalpr device object for processing stream/images.""" - - def __init__(self, name, interval, api, input_source, - extra_arguments=None): - """Init image processing.""" - from haffmpeg import ImageStream, ImageSingle - - super().__init__(name, interval, api) - self._input_source = input_source - self._extra_arguments = extra_arguments - - if self._interval > 0: - self._ffmpeg = ImageStream(get_binary(), self._process_image) - else: - self._ffmpeg = ImageSingle(get_binary()) - - self._start_ffmpeg() - - def shutdown(self, event): - """Close ffmpeg stream.""" - if self._interval > 0: - self._ffmpeg.close() - - def restart(self): - """Restart ffmpeg stream.""" - if self._interval > 0: - self._ffmpeg.close() - self._start_ffmpeg() - - def scan(self): - """Immediately scan a image.""" - from haffmpeg import IMAGE_PNG - - # process single image - if self._interval == 0: - image = self._ffmpeg.get_image( - self._input_source, - output_format=IMAGE_PNG, - extra_cmd=self._extra_arguments - ) - return self._process_image(image) - - # stream - self._ffmpeg.push_image() - - def _start_ffmpeg(self): - """Start a ffmpeg image stream.""" - from haffmpeg import IMAGE_PNG - if self._interval == 0: - return - - self._ffmpeg.open_stream( - input_source=self._input_source, - interval=self._interval, - output_format=IMAGE_PNG, - extra_cmd=self._extra_arguments, - ) - - @property - def should_poll(self): - """Return True if render is be 'image' or False if 'ffmpeg'.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self._interval == 0 or self._ffmpeg.is_running - - -class OpenalprDeviceImage(OpenalprDevice): - """Represent a openalpr device object for processing stream/images.""" - - def __init__(self, name, interval, api, input_source, - username=None, password=None): - """Init image processing.""" - super().__init__(name, interval, api) - - self._next = time() - self._username = username - self._password = password - self._url = input_source - - def restart(self): - """Fake restart with scan a picture.""" - self.scan() - - def scan(self): - """Immediately scan a image.""" - # send request - if self._username is not None and self._password is not None: - req = requests.get( - self._url, auth=(self._username, self._password), timeout=15) - else: - req = requests.get(self._url, timeout=15) - - # process image - image = req.content - self._process_image(image) - - @property - def should_poll(self): - """Return True if render is be 'image' or False if 'ffmpeg'.""" - return self._interval > 0 - - def update(self): - """Retrieve latest state.""" - if self._next > time(): - return - self.scan() - self._next = time() + self._interval - - -class OpenalprApi(object): - """OpenAlpr api class.""" - - def __init__(self, region, confidence): - """Init basic api processing.""" - self._region = region - self._confidence = confidence - - def process_image(self, image, event_callback): - """Callback for processing image.""" - raise NotImplementedError() - - -class OpenalprApiCloud(OpenalprApi): - """Use the cloud openalpr api to parse licences plate.""" - - def __init__(self, region, confidence, api_key): - """Init cloud api processing.""" - import openalpr_api - - super().__init__(region=region, confidence=confidence) - self._api = openalpr_api.DefaultApi() - self._api_key = api_key - - def process_image(self, image, event_callback): - """Callback for processing image.""" - result = self._api.recognize_post( - self._api_key, - 'plate', - image="", - image_bytes=str(b64encode(image), 'utf-8'), - country=self._region - ) - - # process result - f_plates = {} - # pylint: disable=no-member - for object_plate in result.plate.results: - plate = object_plate.plate - confidence = object_plate.confidence - if confidence >= self._confidence: - f_plates[plate] = confidence - event_callback(f_plates) - - -class OpenalprApiLocal(OpenalprApi): - """Use local openalpr library to parse licences plate.""" - - def __init__(self, region, confidence, binary): - """Init local api processing.""" - # pylint: disable=import-error - from haalpr import HAAlpr - - super().__init__(region=region, confidence=confidence) - self._api = HAAlpr(binary=binary, country=region) - - def process_image(self, image, event_callback): - """Callback for processing image.""" - result = self._api.recognize_byte(image) - - # process result - f_plates = {} - for found in result: - for plate, confidence in found.items(): - if confidence >= self._confidence: - f_plates[plate] = confidence - event_callback(f_plates) diff --git a/requirements_all.txt b/requirements_all.txt index 4ef334dc632..ec328eca44d 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,9 +169,6 @@ googlemaps==2.4.4 # homeassistant.components.sensor.gpsd gps3==0.33.3 -# homeassistant.components.openalpr -ha-alpr==0.3 - # homeassistant.components.ffmpeg ha-ffmpeg==0.15 @@ -244,9 +241,6 @@ https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a8 # homeassistant.components.notify.joaoapps_join https://github.com/nkgilley/python-join-api/archive/3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1 -# homeassistant.components.openalpr -https://github.com/pvizeli/cloudapi/releases/download/1.0.2/python-1.0.2.zip#openalpr_api==1.0.2 - # homeassistant.components.switch.edimax https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 From 5dd45efac3a1774f39d717cb79b268b3395049da Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 20 Jan 2017 01:22:33 -0500 Subject: [PATCH 069/191] Updated ISY component to not overwrite state_attributes. (#5433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated ISY component to not overwrite state_attributes. The ISY component included an ISYDevice base class that is used by all of the isy994 platforms. This still overwrote the state_attributes property instead of the more appropriate device_state_attributes property. This was also repeated in the isy994 light platform. Both of these were addressed. This also fixes issue #5428. * Removed custom state attributes from ISY lights. The brightness attribute need not be manually reported by the isy994 light platform. * Removed ISY Node cleanup. The ISY entities don’t really need to unsubscribe themselves while hass is shutting down. Because these updates are not sent in a thread, there is no negative impact from shutting down without unsubscribing. This greatly speeds up hass shutdown. * Removed unused attribute from isy994 light platform. * Cleaned up ISY994 light entity class. 1) Removed the state property. This property is set in the Entity base class and shouldn’t be overridden here. 2) Set the brightness property. This is the proper way of setting the brightness for the Light base class. 3) Removed properties that are now unused because of these changes. --- homeassistant/components/isy994.py | 6 +----- homeassistant/components/light/isy994.py | 22 ++++++---------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 7451b3286f7..cbe7c7166e7 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -228,10 +228,6 @@ class ISYDevice(Entity): self._change_handler = self._node.status.subscribe('changed', self.on_update) - def __del__(self) -> None: - """Cleanup the subscriptions.""" - self._change_handler.unsubscribe() - # pylint: disable=unused-argument def on_update(self, event: object) -> None: """Handle the update event from the ISY994 Node.""" @@ -272,7 +268,7 @@ class ISYDevice(Entity): return self._node.status._val @property - def state_attributes(self) -> Dict: + def device_state_attributes(self) -> Dict: """Get the state attributes for the device.""" attr = {} if hasattr(self._node, 'aux_properties'): diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 952c52b2809..1cde50de820 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -8,18 +8,13 @@ import logging from typing import Callable from homeassistant.components.light import ( - Light, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS) + Light, SUPPORT_BRIGHTNESS) import homeassistant.components.isy994 as isy -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN +from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -VALUE_TO_STATE = { - False: STATE_OFF, - True: STATE_ON, -} - UOM = ['2', '51', '78'] STATES = [STATE_OFF, STATE_ON, 'true', 'false', '%'] @@ -52,12 +47,12 @@ class ISYLightDevice(isy.ISYDevice, Light): @property def is_on(self) -> bool: """Get whether the ISY994 light is on.""" - return self.state == STATE_ON + return self.value > 0 @property - def state(self) -> str: - """Get the state of the ISY994 light.""" - return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN) + def brightness(self) -> float: + """Get the brightness of the ISY994 light.""" + return self.value def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" @@ -69,11 +64,6 @@ class ISYLightDevice(isy.ISYDevice, Light): if not self._node.on(val=brightness): _LOGGER.debug('Unable to turn on light.') - @property - def state_attributes(self): - """Flag supported attributes.""" - return {ATTR_BRIGHTNESS: self.value} - @property def supported_features(self): """Flag supported features.""" From 1f6f9a1677ea2abbe7bc9913f4b5ee16a16c750e Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 20 Jan 2017 02:30:47 -0500 Subject: [PATCH 070/191] Filter new entities from logbook (#5402) --- homeassistant/components/logbook.py | 4 +++ tests/components/test_logbook.py | 44 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 94445935093..b69289db989 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -307,6 +307,10 @@ def _exclude_events(events, config): if event.event_type == EVENT_STATE_CHANGED: to_state = State.from_dict(event.data.get('new_state')) # Do not report on new entities + if event.data.get('old_state') is None: + continue + + # Do not report on entity removal if not to_state: continue diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index dcb675e00e5..047ca480f6f 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -117,6 +117,50 @@ class TestComponentLogbook(unittest.TestCase): self.assertEqual(0, len(entries)) + def test_exclude_new_entities(self): + """Test if events are excluded on first update.""" + entity_id = 'sensor.bla' + entity_id2 = 'sensor.blu' + pointA = dt_util.utcnow() + pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) + + eventA = self.create_state_changed_event(pointA, entity_id, 10) + eventB = self.create_state_changed_event(pointB, entity_id2, 20) + eventA.data['old_state'] = None + + events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), + eventA, eventB), self.EMPTY_CONFIG) + entries = list(logbook.humanify(events)) + + self.assertEqual(2, len(entries)) + self.assert_entry( + entries[0], name='Home Assistant', message='stopped', + domain=ha.DOMAIN) + self.assert_entry( + entries[1], pointB, 'blu', domain='sensor', entity_id=entity_id2) + + def test_exclude_removed_entities(self): + """Test if events are excluded on last update.""" + entity_id = 'sensor.bla' + entity_id2 = 'sensor.blu' + pointA = dt_util.utcnow() + pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) + + eventA = self.create_state_changed_event(pointA, entity_id, 10) + eventB = self.create_state_changed_event(pointB, entity_id2, 20) + eventA.data['new_state'] = None + + events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), + eventA, eventB), self.EMPTY_CONFIG) + entries = list(logbook.humanify(events)) + + self.assertEqual(2, len(entries)) + self.assert_entry( + entries[0], name='Home Assistant', message='stopped', + domain=ha.DOMAIN) + self.assert_entry( + entries[1], pointB, 'blu', domain='sensor', entity_id=entity_id2) + def test_exclude_events_hidden(self): """Test if events are excluded if entity is hidden.""" entity_id = 'sensor.bla' From 2ed0e76e7cb4ef963a18351300f479d82abb37c2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 20 Jan 2017 08:55:29 +0100 Subject: [PATCH 071/191] Add elevation to as_dict and use unified style for quoting (#5448) --- homeassistant/core.py | 49 +++++++++++++++++++++---------------------- tests/test_core.py | 1 + 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7fd6006f916..e2831a63d75 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -43,7 +43,7 @@ try: except ImportError: pass -DOMAIN = "homeassistant" +DOMAIN = 'homeassistant' # How often time_changed event should fire TIMER_INTERVAL = 1 # seconds @@ -88,10 +88,10 @@ def is_callback(func: Callable[..., Any]) -> bool: class CoreState(enum.Enum): """Represent the current state of Home Assistant.""" - not_running = "NOT_RUNNING" - starting = "STARTING" - running = "RUNNING" - stopping = "STOPPING" + not_running = 'NOT_RUNNING' + starting = 'STARTING' + running = 'RUNNING' + stopping = 'STOPPING' def __str__(self) -> str: """Return the event.""" @@ -103,7 +103,7 @@ class HomeAssistant(object): def __init__(self, loop=None): """Initialize new Home Assistant object.""" - if sys.platform == "win32": + if sys.platform == 'win32': self.loop = loop or asyncio.ProactorEventLoop() else: self.loop = loop or asyncio.get_event_loop() @@ -164,13 +164,13 @@ class HomeAssistant(object): self.loop.add_signal_handler( signal.SIGTERM, self._async_stop_handler) except ValueError: - _LOGGER.warning('Could not bind to SIGTERM.') + _LOGGER.warning("Could not bind to SIGTERM") try: self.loop.add_signal_handler( signal.SIGHUP, self._async_restart_handler) except ValueError: - _LOGGER.warning('Could not bind to SIGHUP.') + _LOGGER.warning("Could not bind to SIGHUP") # pylint: disable=protected-access self.loop._thread_ident = threading.get_ident() @@ -185,7 +185,7 @@ class HomeAssistant(object): args: parameters for method to call. """ if target is None: - raise ValueError("Don't call add_job with None.") + raise ValueError("Don't call add_job with None") self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback @@ -322,8 +322,7 @@ class HomeAssistant(object): kwargs['exc_info'] = (type(exception), exception, exception.__traceback__) - _LOGGER.error('Error doing job: %s', context['message'], - **kwargs) + _LOGGER.error("Error doing job: %s", context['message'], **kwargs) @callback def _async_stop_handler(self, *args): @@ -341,8 +340,8 @@ class HomeAssistant(object): class EventOrigin(enum.Enum): """Represent the origin of an event.""" - local = "LOCAL" - remote = "REMOTE" + local = 'LOCAL' + remote = 'REMOTE' def __str__(self): """Return the event.""" @@ -420,8 +419,8 @@ class EventBus(object): def fire(self, event_type: str, event_data=None, origin=EventOrigin.local): """Fire an event.""" - self._hass.loop.call_soon_threadsafe(self.async_fire, event_type, - event_data, origin) + self._hass.loop.call_soon_threadsafe( + self.async_fire, event_type, event_data, origin) @callback def async_fire(self, event_type: str, event_data=None, @@ -432,7 +431,7 @@ class EventBus(object): """ if event_type != EVENT_HOMEASSISTANT_STOP and \ self._hass.state == CoreState.stopping: - raise ShuttingDown('Home Assistant is shutting down.') + raise ShuttingDown("Home Assistant is shutting down") # Copy the list of the current listeners because some listeners # remove themselves as a listener while being executed which @@ -549,8 +548,7 @@ class EventBus(object): except (KeyError, ValueError): # KeyError is key event_type listener did not exist # ValueError if listener did not exist within event_type - _LOGGER.warning('Unable to remove unknown listener %s', - listener) + _LOGGER.warning("Unable to remove unknown listener %s", listener) class State(object): @@ -995,14 +993,14 @@ class ServiceRegistry(object): if event.data[ATTR_SERVICE_CALL_ID] == call_id: fut.set_result(True) - unsub = self._hass.bus.async_listen(EVENT_SERVICE_EXECUTED, - service_executed) + unsub = self._hass.bus.async_listen( + EVENT_SERVICE_EXECUTED, service_executed) self._hass.bus.async_fire(EVENT_CALL_SERVICE, event_data) if blocking: - done, _ = yield from asyncio.wait([fut], loop=self._hass.loop, - timeout=SERVICE_CALL_LIMIT) + done, _ = yield from asyncio.wait( + [fut], loop=self._hass.loop, timeout=SERVICE_CALL_LIMIT) success = bool(done) unsub() return success @@ -1017,7 +1015,7 @@ class ServiceRegistry(object): if not self.has_service(domain, service): if event.origin == EventOrigin.local: - _LOGGER.warning('Unable to find service %s/%s', + _LOGGER.warning("Unable to find service %s/%s", domain, service) return @@ -1040,7 +1038,7 @@ class ServiceRegistry(object): if service_handler.schema: service_data = service_handler.schema(service_data) except vol.Invalid as ex: - _LOGGER.error('Invalid service data for %s.%s: %s', + _LOGGER.error("Invalid service data for %s.%s: %s", domain, service, humanize_error(service_data, ex)) fire_service_executed() return @@ -1064,7 +1062,7 @@ class ServiceRegistry(object): def _generate_unique_id(self): """Generate a unique service call id.""" self._cur_id += 1 - return "{}-{}".format(id(self), self._cur_id) + return '{}-{}'.format(id(self), self._cur_id) class Config(object): @@ -1118,6 +1116,7 @@ class Config(object): return { 'latitude': self.latitude, 'longitude': self.longitude, + 'elevation': self.elevation, 'unit_system': self.units.as_dict(), 'location_name': self.location_name, 'time_zone': time_zone.zone, diff --git a/tests/test_core.py b/tests/test_core.py index 4049d10d32d..14276584ae2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -724,6 +724,7 @@ class TestConfig(unittest.TestCase): expected = { 'latitude': None, 'longitude': None, + 'elevation': None, CONF_UNIT_SYSTEM: METRIC_SYSTEM.as_dict(), 'location_name': None, 'time_zone': 'UTC', From f669680b1e393490cbf6c6567e646aac15c73858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 20 Jan 2017 08:58:15 +0100 Subject: [PATCH 072/191] fix issue ##5398 in yr sensor (#5459) --- homeassistant/components/sensor/yr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 7da72b6fd38..96b67776000 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -201,6 +201,7 @@ class YrData(object): for time_entry in self.data['product']['time']: valid_from = dt_util.parse_datetime(time_entry['@from']) valid_to = dt_util.parse_datetime(time_entry['@to']) + new_state = None loc_data = time_entry['location'] From 8496975de887226ab5cf37c35aea8e661dc830da Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Jan 2017 09:07:03 +0100 Subject: [PATCH 073/191] Fix if none data is present for a sensor. (#5415) --- homeassistant/components/sensor/netatmo.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 20c0f94a500..41fc4287f5f 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -10,7 +10,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.loader import get_component @@ -142,7 +142,12 @@ class NetAtmoSensor(Entity): def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() - data = self.netatmo_data.data[self.module_name] + data = self.netatmo_data.data.get(self.module_name) + + if data is None: + _LOGGER.warning("No data found for %s", self.module_name) + self._state = STATE_UNKNOWN + return if self.type == 'temperature': self._state = round(data['Temperature'], 1) From c41cf7c3083bb4e05adee0c11d9f5e9f9a4fd7f9 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Fri, 20 Jan 2017 13:35:41 +0100 Subject: [PATCH 074/191] Bugfix Zwave Light: Use only supported features for devices (#5370) * Use only supported features for devices * Changes * Holy Macarony! --- homeassistant/components/light/zwave.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index d973f8d8dd2..754c27cbad3 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -42,7 +42,10 @@ TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN -SUPPORT_ZWAVE = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR +SUPPORT_ZWAVE_DIMMER = SUPPORT_BRIGHTNESS +SUPPORT_ZWAVE_COLOR = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR +SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + | SUPPORT_COLOR_TEMP) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -161,7 +164,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_ZWAVE + return SUPPORT_ZWAVE_DIMMER def turn_on(self, **kwargs): """Turn the device on.""" @@ -351,3 +354,11 @@ class ZwaveColorLight(ZwaveDimmer): self._value_color.node.set_rgbw(self._value_color.value_id, rgbw) super().turn_on(**kwargs) + + @property + def supported_features(self): + """Flag supported features.""" + if self._zw098: + return SUPPORT_ZWAVE_COLORTEMP + else: + return SUPPORT_ZWAVE_COLOR From a7e5c847fb1afb20d0c3698ca6e92beb864b0444 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 20 Jan 2017 14:52:55 -0500 Subject: [PATCH 075/191] Kodi supports volume stepping (#5467) --- homeassistant/components/media_player/kodi.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 8cfa7a587fb..790bfb3c724 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice, + PLATFORM_SCHEMA) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD) @@ -35,7 +36,7 @@ TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, From 5ff9dfa44022bdb8e71977d4cffef9d4b65f949b Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 20 Jan 2017 15:21:27 -0500 Subject: [PATCH 076/191] Use voluptuous for cast ignore-cec (#5468) --- homeassistant/components/media_player/cast.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index faa204e675e..202c877c2b1 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -37,6 +37,7 @@ KNOWN_HOSTS = [] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_IGNORE_CEC): [cv.string], }) @@ -46,11 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pychromecast # import CEC IGNORE attributes - ignore_cec = config.get(CONF_IGNORE_CEC, []) - if isinstance(ignore_cec, list): - pychromecast.IGNORE_CEC += ignore_cec - else: - _LOGGER.error('CEC config "%s" must be a list.', CONF_IGNORE_CEC) + pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) hosts = [] From cb47d16282809afab69ff2f16cdcdbec7923d3a8 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Fri, 20 Jan 2017 15:30:09 -0500 Subject: [PATCH 077/191] Don't use Debian's httpredir for backports (#5392) Hopefully this solves https://github.com/home-assistant/home-assistant/pull/5322#issuecomment-273041585 --- script/setup_docker_prereqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/setup_docker_prereqs b/script/setup_docker_prereqs index f0c6ddf4cc5..a482d9a0ec7 100755 --- a/script/setup_docker_prereqs +++ b/script/setup_docker_prereqs @@ -35,7 +35,7 @@ echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.l wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - # Add jessie-backports -echo "deb http://httpredir.debian.org/debian jessie-backports main" >> /etc/apt/sources.list +echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list # Install packages apt-get update From 067e11ea5c601021588627c59a3d5ef1cdf0a369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Vran=C3=ADk?= Date: Fri, 20 Jan 2017 21:39:18 +0100 Subject: [PATCH 078/191] HDMI CEC - support for devices and commands (#4781) * cec client object * cec command structure * autodetect source * volume support and native source select * switch device * media player device * detecting of state * friendly names * hdmi cec properties * presence detection * simplified callbacks * stable names * renamed methods * code cleanup * name with vendor * fixed standby call name * fake standby/poweron * domain switch * domain switch * async updating * update separated * cec -> hass event bridge * fixed name generation * code cleanup * code cleanup * icon constants * code cleanup * do not register unavailable devices * discovery of deevices * code cleanup * cec device discovery * moved method implementation into child * service descriptions * service descriptions * service descriptions * changed entity init sequence * logging cleanup * add remove as job * closing cec, no service schemas * correct iterate over dictionary * Volume by commands * threading * logging minimized * get load out of main thread * naming cleanup * get load out of main thread * optimized discovery * async where possible * cleanup logging, constructors first * pydoc * formatting * no async_update from out of loop no hiding entities removed redundant device_state_attributes async updating presence * no async * working async cec * cec in thirdparty lib * cec initialized oudsice * working without SIGSEGV * rollbacked file changed by mistake * sending of commands * working with ha * using hass loop and device driven updates * version up * version up * Command types in pycec, cleanup for HA integration * Removed media player, state moved to switch * service descriptions * requirements: pyCEC * line width to 79 * doc * doc * overindentation solved * HDMI to uppercase * minimal dependency on cec * removed unwanted line * doc wording * margin 79 * line continuation indent * imperative doc * lint: indentation * fixed overindented * fixed overindented * fixed overindented * fixed overindented * order of imports * PEP8 * keep signature of overriding * removed redundant blank line * fixed update call method (#4) * Preparation for merge to upstream (#5) * newer version of pyCEC * updated services.yaml * fixed lint scrpt to operate only on python files * pycec version up * update services * no coverage report * exclude non python files from lint * lint only on python files * Dev (#6) * reordered * sending nonserialized data through hass.data * code formatting * code formatting * import order * Dev (#7) * newer version of pyCEC * updated services.yaml * fixed lint scrpt to operate only on python files * pycec version up * update services * no coverage report * exclude non python files from lint * lint only on python files * reordered * sending nonserialized data through hass.data * import order * fixed object handling * code formatting * Backwards compatibility of hdmi_cec (#10) * services: power_on standby active_source * new version of pyCEC (#12) * newer version of pyCEC * devices config (#13) * getting device name from config * shutdown fix (#14) * correct call on shutdown * remove misplaced annotations (#15) * Preparation for merge to upstream (#5) * newer version of pyCEC * updated services.yaml * reordered * sending nonserialized data through hass.data * services: power_on standby active_source * code formatting * getting device name from config * correct call on shutdown * pyCEC version 0.3.6 (#18) * newer version of pyCEC * updated services.yaml * sending nonserialized data through hass.data * services: ** power_on ** standby ** active_source * getting device name from config * correct call on shutdown * fork new thread on multicore machines * support both config schemas: original and new (#16) * volume press and release support (#17) * support for media_player (#21) * accept hexadecimal format of commands * support for media player * platform customization * type constants * Dev (#23) * accept hexadecimal format of commands * support for media player * platform customization * TCP CEC support (#24) * accept hexadecimal format of commands * support for media player * platform customization * preparing tcp support * volume handling (#25) * Incorporated CR remarks (#26) * cleanup imports * cleanup and enhance services description * removed unwanted file * implemented CR remarks (#27) * pyCEC v0.4.6 * pined dependency version * tighten service schemas * requirements (#28) --- .coveragerc | 2 + homeassistant/components/hdmi_cec.py | 425 +++++++++++++++--- .../components/media_player/hdmi_cec.py | 175 ++++++++ homeassistant/components/services.yaml | 55 +++ homeassistant/components/switch/hdmi_cec.py | 63 +++ requirements_all.txt | 3 + 6 files changed, 658 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/media_player/hdmi_cec.py create mode 100644 homeassistant/components/switch/hdmi_cec.py diff --git a/.coveragerc b/.coveragerc index 5bd097e8abb..ea0530aa8f7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -214,6 +214,7 @@ omit = homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py + homeassistant/components/media_player/hdmi_cec.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py @@ -356,6 +357,7 @@ omit = homeassistant/components/switch/digitalloggers.py homeassistant/components/switch/dlink.py homeassistant/components/switch/edimax.py + homeassistant/components/switch/hdmi_cec.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hook.py homeassistant/components/switch/kankun.py diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index 4fab7f84bd3..44b205993b6 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -1,27 +1,110 @@ """ -CEC component. +HDMI CEC component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/hdmi_cec/ """ import logging +import multiprocessing +import os +from collections import defaultdict +from functools import reduce import voluptuous as vol -from homeassistant.const import (EVENT_HOMEASSISTANT_START, CONF_DEVICES) import homeassistant.helpers.config_validation as cv +from homeassistant import core +from homeassistant.components import discovery +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER +from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, + EVENT_HOMEASSISTANT_STOP, STATE_ON, + STATE_OFF, CONF_DEVICES, CONF_PLATFORM, + CONF_CUSTOMIZE, STATE_PLAYING, STATE_IDLE, + STATE_PAUSED, CONF_HOST) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity -_CEC = None -_LOGGER = logging.getLogger(__name__) - -ATTR_DEVICE = 'device' +REQUIREMENTS = ['pyCEC==0.4.6'] DOMAIN = 'hdmi_cec' -MAX_DEPTH = 4 +_LOGGER = logging.getLogger(__name__) + +ICON_UNKNOWN = 'mdi:help' +ICON_AUDIO = 'mdi:speaker' +ICON_PLAYER = 'mdi:play' +ICON_TUNER = 'mdi:nest-thermostat' +ICON_RECORDER = 'mdi:microphone' +ICON_TV = 'mdi:television' +ICONS_BY_TYPE = { + 0: ICON_TV, + 1: ICON_RECORDER, + 3: ICON_TUNER, + 4: ICON_PLAYER, + 5: ICON_AUDIO +} + +CEC_DEVICES = defaultdict(list) + +CMD_UP = 'up' +CMD_DOWN = 'down' +CMD_MUTE = 'mute' +CMD_UNMUTE = 'unmute' +CMD_MUTE_TOGGLE = 'toggle mute' +CMD_PRESS = 'press' +CMD_RELEASE = 'release' + +EVENT_CEC_COMMAND_RECEIVED = 'cec_command_received' +EVENT_CEC_KEYPRESS_RECEIVED = 'cec_keypress_received' + +ATTR_PHYSICAL_ADDRESS = 'physical_address' +ATTR_TYPE_ID = 'type_id' +ATTR_VENDOR_NAME = 'vendor_name' +ATTR_VENDOR_ID = 'vendor_id' +ATTR_DEVICE = 'device' +ATTR_COMMAND = 'command' +ATTR_TYPE = 'type' +ATTR_KEY = 'key' +ATTR_DUR = 'dur' +ATTR_SRC = 'src' +ATTR_DST = 'dst' +ATTR_CMD = 'cmd' +ATTR_ATT = 'att' +ATTR_RAW = 'raw' +ATTR_DIR = 'dir' +ATTR_ABT = 'abt' +ATTR_NEW = 'new' + +_VOL_HEX = vol.Any(vol.Coerce(int), lambda x: int(x, 16)) + +SERVICE_SEND_COMMAND = 'send_command' +SERVICE_SEND_COMMAND_SCHEMA = vol.Schema({ + vol.Optional(ATTR_CMD): _VOL_HEX, + vol.Optional(ATTR_SRC): _VOL_HEX, + vol.Optional(ATTR_DST): _VOL_HEX, + vol.Optional(ATTR_ATT): _VOL_HEX, + vol.Optional(ATTR_RAW): vol.Coerce(str) +}, extra=vol.PREVENT_EXTRA) + +SERVICE_VOLUME = 'volume' +SERVICE_VOLUME_SCHEMA = vol.Schema({ + vol.Optional(CMD_UP): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), + vol.Optional(CMD_DOWN): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), + vol.Optional(CMD_MUTE): None, + vol.Optional(CMD_UNMUTE): None, + vol.Optional(CMD_MUTE_TOGGLE): None +}, extra=vol.PREVENT_EXTRA) + +SERVICE_UPDATE_DEVICES = 'update' +SERVICE_UPDATE_DEVICES_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({}) +}, extra=vol.PREVENT_EXTRA) + +SERVICE_SELECT_DEVICE = 'select_device' SERVICE_POWER_ON = 'power_on' -SERVICE_SELECT_DEVICE = 'select_device' SERVICE_STANDBY = 'standby' # pylint: disable=unnecessary-lambda @@ -30,92 +113,304 @@ DEVICE_SCHEMA = vol.Schema({ cv.string) }) +CUSTOMIZE_SCHEMA = vol.Schema({ + vol.Optional(CONF_PLATFORM, default=MEDIA_PLAYER): vol.Any(MEDIA_PLAYER, + SWITCH) +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICES): DEVICE_SCHEMA + vol.Optional(CONF_DEVICES): vol.Any(DEVICE_SCHEMA, + vol.Schema({ + vol.All(cv.string): vol.Any( + cv.string) + })), + vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER), + vol.Optional(CONF_HOST): cv.string, }) }, extra=vol.ALLOW_EXTRA) +def pad_physical_address(addr): + """Right-pad a physical address.""" + return addr + [0] * (4 - len(addr)) + + def parse_mapping(mapping, parents=None): """Parse configuration device mapping.""" if parents is None: parents = [] for addr, val in mapping.items(): - cur = parents + [str(addr)] - if isinstance(val, dict): - yield from parse_mapping(val, cur) - elif isinstance(val, str): - yield (val, cur) + if isinstance(addr, (str,)) and isinstance(val, (str,)): + from pycec.network import PhysicalAddress + yield (addr, PhysicalAddress(val)) + else: + cur = parents + [addr] + if isinstance(val, dict): + yield from parse_mapping(val, cur) + elif isinstance(val, str): + yield (val, pad_physical_address(cur)) -def pad_physical_address(addr): - """Right-pad a physical address.""" - return addr + ['0'] * (MAX_DEPTH - len(addr)) - - -def setup(hass, config): +def setup(hass: HomeAssistant, base_config): """Setup CEC capability.""" - global _CEC - - try: - import cec - except ImportError: - _LOGGER.error("libcec must be installed") - return False + from pycec.network import HDMINetwork + from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand + from pycec.const import KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_MUTE, \ + ADDR_AUDIOSYSTEM, ADDR_BROADCAST, ADDR_UNREGISTERED + from pycec.cec import CecAdapter + from pycec.tcp import TcpAdapter # Parse configuration into a dict of device name to physical address # represented as a list of four elements. - flat = {} - for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})): - flat[pair[0]] = pad_physical_address(pair[1]) + device_aliases = {} + devices = base_config[DOMAIN].get(CONF_DEVICES, {}) + _LOGGER.debug("Parsing config %s", devices) + device_aliases.update(parse_mapping(devices)) + _LOGGER.debug("Parsed devices: %s", device_aliases) - # Configure libcec. - cfg = cec.libcec_configuration() - cfg.strDeviceName = 'HASS' - cfg.bActivateSource = 0 - cfg.bMonitorOnly = 1 - cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT + platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH) - # Setup CEC adapter. - _CEC = cec.ICECAdapter.Create(cfg) + loop = ( + # Create own thread if more than 1 CPU + hass.loop if multiprocessing.cpu_count() < 2 else None) + host = base_config[DOMAIN].get(CONF_HOST, None) + if host: + adapter = TcpAdapter(host, name="HASS", activate_source=False) + else: + adapter = CecAdapter(name="HASS", activate_source=False) + hdmi_network = HDMINetwork(adapter, loop=loop) - def _power_on(call): - """Power on all devices.""" - _CEC.PowerOnDevices() + def _volume(call): + """Increase/decrease volume and mute/unmute system.""" + for cmd, att in call.data.items(): + if cmd == CMD_UP: + _process_volume(KEY_VOLUME_UP, att) + elif cmd == CMD_DOWN: + _process_volume(KEY_VOLUME_DOWN, att) + elif cmd == CMD_MUTE: + hdmi_network.send_command( + KeyPressCommand(KEY_MUTE, dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command( + KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + _LOGGER.info("Audio muted") + else: + _LOGGER.warning("Unknown command %s", cmd) + def _process_volume(cmd, att): + if isinstance(att, (str,)): + att = att.strip() + if att == CMD_PRESS: + hdmi_network.send_command( + KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) + elif att == CMD_RELEASE: + hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + else: + att = 1 if att == "" else int(att) + for _ in range(1, att): + hdmi_network.send_command( + KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command( + KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + + def _tx(call): + """Send CEC command.""" + data = call.data + if ATTR_RAW in data: + command = CecCommand(data[ATTR_RAW]) + else: + if ATTR_SRC in data: + src = data[ATTR_SRC] + else: + src = ADDR_UNREGISTERED + if ATTR_DST in data: + dst = data[ATTR_DST] + else: + dst = ADDR_BROADCAST + if ATTR_CMD in data: + cmd = data[ATTR_CMD] + else: + _LOGGER.error("Attribute 'cmd' is missing") + return False + if ATTR_ATT in data: + if isinstance(data[ATTR_ATT], (list,)): + att = data[ATTR_ATT] + else: + att = reduce(lambda x, y: "%s:%x" % (x, y), data[ATTR_ATT]) + else: + att = "" + command = CecCommand(cmd, dst, src, att) + hdmi_network.send_command(command) + + @callback def _standby(call): - """Standby all devices.""" - _CEC.StandbyDevices() + hdmi_network.standby() + + @callback + def _power_on(call): + hdmi_network.power_on() def _select_device(call): """Select the active device.""" - path = flat.get(call.data[ATTR_DEVICE]) - if not path: + from pycec.network import PhysicalAddress + + addr = call.data[ATTR_DEVICE] + if not addr: _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) - cmds = [] - for i in range(1, MAX_DEPTH - 1): - addr = pad_physical_address(path[:i]) - cmds.append('1f:82:{}{}:{}{}'.format(*addr)) - cmds.append('1f:86:{}{}:{}{}'.format(*addr)) - for cmd in cmds: - _CEC.Transmit(_CEC.CommandFromString(cmd)) - _LOGGER.info("Selected %s", call.data[ATTR_DEVICE]) + return + if addr in device_aliases: + addr = device_aliases[addr] + else: + entity = hass.states.get(addr) + _LOGGER.debug("Selecting entity %s", entity) + if entity is not None: + addr = entity.attributes['physical_address'] + _LOGGER.debug("Address acquired: %s", addr) + if addr is None: + _LOGGER.error("Device %s has not physical address.", + call.data[ATTR_DEVICE]) + return + if not isinstance(addr, (PhysicalAddress,)): + addr = PhysicalAddress(addr) + hdmi_network.active_source(addr) + _LOGGER.info("Selected %s (%s)", call.data[ATTR_DEVICE], addr) + + def _update(call): + """ + Callback called when device update is needed. + + - called by service, requests CEC network to update data. + """ + hdmi_network.scan() + + @callback + def _new_device(device): + """Called when new device is detected by HDMI network.""" + key = DOMAIN + '.' + device.name + hass.data[key] = device + discovery.load_platform(hass, base_config.get(core.DOMAIN).get( + CONF_CUSTOMIZE, {}).get(key, {}).get(CONF_PLATFORM, platform), + DOMAIN, discovered={ATTR_NEW: [key]}, + hass_config=base_config) + + def _shutdown(call): + hdmi_network.stop() def _start_cec(event): - """Open CEC adapter.""" - adapters = _CEC.DetectAdapters() - if len(adapters) == 0: - _LOGGER.error("No CEC adapter found") - return + """Register services and start HDMI network to watch for devices.""" + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml'))[DOMAIN] + hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx, + descriptions[SERVICE_SEND_COMMAND], + SERVICE_SEND_COMMAND_SCHEMA) + hass.services.register(DOMAIN, SERVICE_VOLUME, _volume, + descriptions[SERVICE_VOLUME], + SERVICE_VOLUME_SCHEMA) + hass.services.register(DOMAIN, SERVICE_UPDATE_DEVICES, _update, + descriptions[SERVICE_UPDATE_DEVICES], + SERVICE_UPDATE_DEVICES_SCHEMA) + hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) + hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) + hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, _select_device) - if _CEC.Open(adapters[0].strComName): - hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) - hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) - hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, - _select_device) - else: - _LOGGER.error("Failed to open adapter") + hdmi_network.set_new_device_callback(_new_device) + hdmi_network.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) return True + + +class CecDevice(Entity): + """Representation of a HDMI CEC device entity.""" + + def __init__(self, hass: HomeAssistant, device, logical): + """Initialize the device.""" + self._device = device + self.hass = hass + self._icon = None + self._state = STATE_UNKNOWN + self._logical_address = logical + self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) + device.set_update_callback(self._update) + + def update(self): + """Update device status.""" + self._update() + + def _update(self, device=None): + """Update device status.""" + if device: + from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ + POWER_OFF, POWER_ON + if device.power_status == POWER_OFF: + self._state = STATE_OFF + elif device.status == STATUS_PLAY: + self._state = STATE_PLAYING + elif device.status == STATUS_STOP: + self._state = STATE_IDLE + elif device.status == STATUS_STILL: + self._state = STATE_PAUSED + elif device.power_status == POWER_ON: + self._state = STATE_ON + else: + _LOGGER.warning("Unknown state: %d", device.power_status) + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the device.""" + return ( + "%s %s" % (self.vendor_name, self._device.osd_name) + if (self._device.osd_name is not None and + self.vendor_name is not None and self.vendor_name != 'Unknown') + else "%s %d" % (self._device.type_name, self._logical_address) + if self._device.osd_name is None + else "%s %d (%s)" % (self._device.type_name, self._logical_address, + self._device.osd_name)) + + @property + def vendor_id(self): + """ID of device's vendor.""" + return self._device.vendor_id + + @property + def vendor_name(self): + """Name of device's vendor.""" + return self._device.vendor + + @property + def physical_address(self): + """Physical address of device in HDMI network.""" + return str(self._device.physical_address) + + @property + def type(self): + """String representation of device's type.""" + return self._device.type_name + + @property + def type_id(self): + """Type ID of device.""" + return self._device.type + + @property + def icon(self): + """Icon for device by its type.""" + return (self._icon if self._icon is not None else + ICONS_BY_TYPE.get(self._device.type) + if self._device.type in ICONS_BY_TYPE else ICON_UNKNOWN) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state_attr = {} + if self.vendor_id is not None: + state_attr[ATTR_VENDOR_ID] = self.vendor_id + state_attr[ATTR_VENDOR_NAME] = self.vendor_name + if self.type_id is not None: + state_attr[ATTR_TYPE_ID] = self.type_id + state_attr[ATTR_TYPE] = self.type + if self.physical_address is not None: + state_attr[ATTR_PHYSICAL_ADDRESS] = self.physical_address + return state_attr diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py new file mode 100644 index 00000000000..4998072018e --- /dev/null +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -0,0 +1,175 @@ +""" +Support for HDMI CEC devices as media players. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hdmi_cec/ +""" +import logging + +from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice +from homeassistant.components.media_player import MediaPlayerDevice, DOMAIN, \ + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, \ + SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_STOP, \ + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE +from homeassistant.const import STATE_ON, STATE_OFF, STATE_PLAYING, \ + STATE_IDLE, STATE_PAUSED +from homeassistant.core import HomeAssistant + +DEPENDENCIES = ['hdmi_cec'] + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return HDMI devices as +switches.""" + if ATTR_NEW in discovery_info: + _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + add_devices(CecPlayerDevice(hass, hass.data.get(device), + hass.data.get(device).logical_address) for + device in discovery_info[ATTR_NEW]) + + +class CecPlayerDevice(CecDevice, MediaPlayerDevice): + """Representation of a HDMI device as a Media palyer.""" + + def __init__(self, hass: HomeAssistant, device, logical): + """Initialize the HDMI device.""" + CecDevice.__init__(self, hass, device, logical) + self.entity_id = "%s.%s_%s" % ( + DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + self.update() + + def send_keypress(self, key): + """Send keypress to CEC adapter.""" + from pycec.commands import KeyPressCommand, KeyReleaseCommand + _LOGGER.debug("Sending keypress %s to device %s", hex(key), + hex(self._logical_address)) + self._device.send_command( + KeyPressCommand(key, dst=self._logical_address)) + self._device.send_command( + KeyReleaseCommand(dst=self._logical_address)) + + def send_playback(self, key): + """Send playback status to CEC adapter.""" + from pycec.commands import CecCommand + self._device.async_send_command( + CecCommand(key, dst=self._logical_address)) + + def mute_volume(self, mute): + """Mute volume.""" + from pycec.const import KEY_MUTE + self.send_keypress(KEY_MUTE) + + def media_previous_track(self): + """Go to previous track.""" + from pycec.const import KEY_BACKWARD + self.send_keypress(KEY_BACKWARD) + + def turn_on(self): + """Turn device on.""" + self._device.turn_on() + self._state = STATE_ON + + def clear_playlist(self): + """Clear players playlist.""" + raise NotImplementedError() + + def turn_off(self): + """Turn device off.""" + self._device.turn_off() + self._state = STATE_OFF + + def media_stop(self): + """Stop playback.""" + from pycec.const import KEY_STOP + self.send_keypress(KEY_STOP) + self._state = STATE_IDLE + + def play_media(self, media_type, media_id): + """Not supported.""" + raise NotImplementedError() + + def media_next_track(self): + """Skip to next track.""" + from pycec.const import KEY_FORWARD + self.send_keypress(KEY_FORWARD) + + def media_seek(self, position): + """Not supported.""" + raise NotImplementedError() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + raise NotImplementedError() + + def media_pause(self): + """Pause playback.""" + from pycec.const import KEY_PAUSE + self.send_keypress(KEY_PAUSE) + self._state = STATE_PAUSED + + def select_source(self, source): + """Not supported.""" + raise NotImplementedError() + + def media_play(self): + """Start playback.""" + from pycec.const import KEY_PLAY + self.send_keypress(KEY_PLAY) + self._state = STATE_PLAYING + + def volume_up(self): + """Increase volume.""" + from pycec.const import KEY_VOLUME_UP + _LOGGER.debug("%s: volume up", self._logical_address) + self.send_keypress(KEY_VOLUME_UP) + + def volume_down(self): + """Decrease volume.""" + from pycec.const import KEY_VOLUME_DOWN + _LOGGER.debug("%s: volume down", self._logical_address) + self.send_keypress(KEY_VOLUME_DOWN) + + @property + def state(self) -> str: + """Cached state of device.""" + return self._state + + def _update(self, device=None): + """Update device status.""" + if device: + from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ + POWER_OFF, POWER_ON + if device.power_status == POWER_OFF: + self._state = STATE_OFF + elif not self.support_pause: + if device.power_status == POWER_ON: + self._state = STATE_ON + elif device.status == STATUS_PLAY: + self._state = STATE_PLAYING + elif device.status == STATUS_STOP: + self._state = STATE_IDLE + elif device.status == STATUS_STILL: + self._state = STATE_PAUSED + else: + _LOGGER.warning("Unknown state: %s", device.status) + self.schedule_update_ha_state() + + @property + def supported_media_commands(self): + """Flag media commands that are supported.""" + from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, \ + TYPE_AUDIO + if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | + SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_PREVIOUS_TRACK | + SUPPORT_NEXT_TRACK) + if self.type == TYPE_TUNER: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | + SUPPORT_PAUSE | SUPPORT_STOP) + if self.type_id == TYPE_AUDIO: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | + SUPPORT_VOLUME_MUTE) + return SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 54c0e18a3ee..53f82d5c059 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -153,3 +153,58 @@ verisure: device_serial: description: The serial number of the smartcam you want to capture an image from. example: '2DEU AT5Z' + +hdmi_cec: + send_command: + description: Sends CEC command into HDMI CEC capable adapter. + + fields: + raw: + description: 'Raw CEC command in format "00:00:00:00" where first two digits are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored.' + example: '"10:36"' + + src: + desctiption: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".' + example: '12 or "0xc"' + + dst: + description: 'Destination for command. Could be decimal number or string with hexadeximal notation: "0x10".' + example: '5 or "0x5"' + + cmd: + description: 'Command itself. Could be decimal number or string with hexadeximal notation: "0x10".' + example: '144 or "0x90"' + + att: + description: Optional parameters. + example: [0, 2] + + update: + description: Update devices state from network. + + volume: + description: Increase or decrease volume of system. + + fields: + up: + description: Increases volume x levels. + example: 3 + down: + description: Decreases volume x levels. + example: 3 + mute: Mutes audio system. Value is ignored. + unmute: Unmutes audio system. Value is ignored. + toggle mute: Toggles mute of audio system. Value is ignored. + + select_device: + description: Select HDMI device. + fields: + device: + description: Addres of device to select. Can be entity_id, physical address or alias from confuguration. + example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"' + + power_on: + description: Power on all devices which supports it. + + standby: + description: Standby all devices which supports it. diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/switch/hdmi_cec.py new file mode 100644 index 00000000000..bd1f9ea6578 --- /dev/null +++ b/homeassistant/components/switch/hdmi_cec.py @@ -0,0 +1,63 @@ +""" +Support for HDMI CEC devices as switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hdmi_cec/ +""" +import logging + +from homeassistant.components.hdmi_cec import CecDevice, ATTR_NEW +from homeassistant.components.switch import SwitchDevice, DOMAIN +from homeassistant.const import STATE_OFF, STATE_STANDBY, STATE_ON +from homeassistant.core import HomeAssistant + +DEPENDENCIES = ['hdmi_cec'] + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return HDMI devices as switches.""" + if ATTR_NEW in discovery_info: + _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + add_devices(CecSwitchDevice(hass, hass.data.get(device), + hass.data.get(device).logical_address) for + device in discovery_info[ATTR_NEW]) + + +class CecSwitchDevice(CecDevice, SwitchDevice): + """Representation of a HDMI device as a Switch.""" + + def __init__(self, hass: HomeAssistant, device, logical): + """Initialize the HDMI device.""" + CecDevice.__init__(self, hass, device, logical) + self.entity_id = "%s.%s_%s" % ( + DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + self.update() + + def turn_on(self, **kwargs) -> None: + """Turn device on.""" + self._device.turn_on() + self._state = STATE_ON + + def turn_off(self, **kwargs) -> None: + """Turn device off.""" + self._device.turn_off() + self._state = STATE_ON + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._state == STATE_ON + + @property + def is_standby(self): + """Return true if device is in standby.""" + return self._state == STATE_OFF or self._state == STATE_STANDBY + + @property + def state(self) -> str: + """Cached state of device.""" + return self._state diff --git a/requirements_all.txt b/requirements_all.txt index ec328eca44d..59927a3672b 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -377,6 +377,9 @@ pwaqi==1.3 # homeassistant.components.sensor.cpuspeed py-cpuinfo==0.2.3 +# homeassistant.components.hdmi_cec +pyCEC==0.4.6 + # homeassistant.components.switch.tplink pyHS100==0.2.3 From f4d2d69a5df172bf4f8a5740194e8d12de703d6d Mon Sep 17 00:00:00 2001 From: Martin Vacula Date: Fri, 20 Jan 2017 21:55:28 +0100 Subject: [PATCH 079/191] Beaglebone Black binary sensor (#5422) * Configuration parameters defined * Edge detection added * Example configuration added and tipo corrected * Comments updated, lint update * Added check for input pull_mode * Too long line * trailing white space * Configuration parameters defined * Edge detection added * Example configuration added and tipo corrected * Comments updated, lint update * Added check for input pull_mode * Too long line * trailing white space * pylint disable import error * read_input() changed to return boolean value, according changes in binary sensor * example configuration in docstring changed to mention direct web link, pylint update * read_input() updated according review --- homeassistant/components/bbb_gpio.py | 2 +- .../components/binary_sensor/bbb_gpio.py | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/binary_sensor/bbb_gpio.py diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio.py index d8acaaa184c..89692a1e1e1 100644 --- a/homeassistant/components/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio.py @@ -63,7 +63,7 @@ def read_input(pin): """Read a value from a GPIO.""" # pylint: disable=import-error,undefined-variable import Adafruit_BBIO.GPIO as GPIO - return GPIO.input(pin) + return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): diff --git a/homeassistant/components/binary_sensor/bbb_gpio.py b/homeassistant/components/binary_sensor/bbb_gpio.py new file mode 100644 index 00000000000..dd960defaa8 --- /dev/null +++ b/homeassistant/components/binary_sensor/bbb_gpio.py @@ -0,0 +1,89 @@ +""" +Support for binary sensor using Beaglebone Black GPIO. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.bbb_gpio/ +""" +import logging + +import voluptuous as vol + +import homeassistant.components.bbb_gpio as bbb_gpio +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bbb_gpio'] + +CONF_PINS = 'pins' +CONF_BOUNCETIME = 'bouncetime' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PULL_MODE = 'pull_mode' + +DEFAULT_BOUNCETIME = 50 +DEFAULT_INVERT_LOGIC = False +DEFAULT_PULL_MODE = 'UP' + +PIN_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): + vol.In(['UP', 'DOWN']) +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS, default={}): + vol.Schema({cv.string: PIN_SCHEMA}), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Beaglebone Black GPIO devices.""" + pins = config.get(CONF_PINS) + + binary_sensors = [] + + for pin, params in pins.items(): + binary_sensors.append(BBBGPIOBinarySensor(pin, params)) + add_devices(binary_sensors) + + +class BBBGPIOBinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses Beaglebone Black GPIO.""" + + def __init__(self, pin, params): + """Initialize the Beaglebone Black binary sensor.""" + self._pin = pin + self._name = params.get(CONF_NAME) or DEVICE_DEFAULT_NAME + self._bouncetime = params.get(CONF_BOUNCETIME) + self._pull_mode = params.get(CONF_PULL_MODE) + self._invert_logic = params.get(CONF_INVERT_LOGIC) + + bbb_gpio.setup_input(self._pin, self._pull_mode) + self._state = bbb_gpio.read_input(self._pin) + + def read_gpio(pin): + """Read state from GPIO.""" + self._state = bbb_gpio.read_input(self._pin) + self.schedule_update_ha_state() + + bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic From 41ee798b0f12b876cb7f7aac2f1b463710d668b3 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Fri, 20 Jan 2017 22:01:36 +0100 Subject: [PATCH 080/191] [WIP][ZWave][Lock] Further improvements to zwave lock platform (#5400) * Further improvements to zwave lock platform * Add missing notification * Some improvements --- homeassistant/components/lock/zwave.py | 103 ++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 17fc30e93cf..9dbbb8e733f 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -14,7 +14,7 @@ from homeassistant.components import zwave _LOGGER = logging.getLogger(__name__) ATTR_NOTIFICATION = 'notification' - +ATTR_LOCK_STATUS = 'lock_status' LOCK_NOTIFICATION = { 1: 'Manual Lock', 2: 'Manual Unlock', @@ -22,18 +22,64 @@ LOCK_NOTIFICATION = { 4: 'RF Unlock', 5: 'Keypad Lock', 6: 'Keypad Unlock', + 11: 'Lock Jammed', 254: 'Unknown Event' } +LOCK_ALARM_TYPE = { + 9: 'Deadbolt Jammed', + 18: 'Locked with Keypad by user', + 19: 'Unlocked with Keypad by user ', + 21: 'Manually Locked by', + 22: 'Manually Unlocked by Key or Inside thumb turn', + 24: 'Locked by RF', + 25: 'Unlocked by RF', + 27: 'Auto re-lock', + 33: 'User deleted: ', + 112: 'Master code changed or User added: ', + 113: 'Duplicate Pin-code: ', + 130: 'RF module, power restored', + 161: 'Tamper Alarm: ', + 167: 'Low Battery', + 168: 'Critical Battery Level', + 169: 'Battery too low to operate' +} + +MANUAL_LOCK_ALARM_LEVEL = { + 1: 'Key Cylinder or Inside thumb turn', + 2: 'Touch function (lock and leave)' +} + +TAMPER_ALARM_LEVEL = { + 1: 'Too many keypresses', + 2: 'Cover removed' +} + LOCK_STATUS = { 1: True, 2: False, 3: True, 4: False, 5: True, - 6: False + 6: False, + 9: False, + 18: True, + 19: False, + 21: True, + 22: False, + 24: True, + 25: False, + 27: True } +ALARM_TYPE_STD = [ + 18, + 19, + 33, + 112, + 113 +] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -68,6 +114,7 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): self._node = value.node self._state = None self._notification = None + self._lock_status = None dispatcher.connect( self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) self.update_properties() @@ -89,9 +136,55 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): self._notification = LOCK_NOTIFICATION.get(value.data) if self._notification: self._state = LOCK_STATUS.get(value.data) + _LOGGER.debug('Lock state set from Access Control value and' + ' is %s', value.data) break - if not self._notification: - self._state = self._value.data + + for value in self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_ALARM).values(): + if value.label != "Alarm Type": + continue + alarm_type = LOCK_ALARM_TYPE.get(value.data) + if alarm_type: + self._state = LOCK_STATUS.get(value.data) + _LOGGER.debug('Lock state set from Alarm Type value and' + ' is %s', value.data) + break + + for value in self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_ALARM).values(): + if value.label != "Alarm Level": + continue + alarm_level = value.data + _LOGGER.debug('Lock alarm_level is %s', alarm_level) + if alarm_type is 21: + self._lock_status = '{}{}'.format( + LOCK_ALARM_TYPE.get(alarm_type), + MANUAL_LOCK_ALARM_LEVEL.get(alarm_level)) + if alarm_type in ALARM_TYPE_STD: + self._lock_status = '{}{}'.format( + LOCK_ALARM_TYPE.get(alarm_type), alarm_level) + break + if alarm_type is 161: + self._lock_status = '{}{}'.format( + LOCK_ALARM_TYPE.get(alarm_type), + TAMPER_ALARM_LEVEL.get(alarm_level)) + break + if alarm_type != 0: + self._lock_status = LOCK_ALARM_TYPE.get(alarm_type) + break + + if not self._notification and not self._lock_status: + for value in self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_DOOR_LOCK).values(): + if value.type != zwave.const.TYPE_BOOL: + continue + if value.genre != zwave.const.GENRE_USER: + continue + self._state = value.data + _LOGGER.debug('Lock state set from Bool value and' + ' is %s', value.data) + break @property def is_locked(self): @@ -112,4 +205,6 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): data = super().device_state_attributes if self._notification: data[ATTR_NOTIFICATION] = self._notification + if self._lock_status: + data[ATTR_LOCK_STATUS] = self._lock_status return data From ec4b148a71c91ff7d926be49ff65fce3a04e1c72 Mon Sep 17 00:00:00 2001 From: Stu Gott Date: Fri, 20 Jan 2017 20:09:03 -0500 Subject: [PATCH 081/191] TTS: Invalidate broken file cache entries If a cached file cannot be read by the TTS component, then it should be removed from the file cache--or it will remain broken. --- homeassistant/components/tts/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 0f731a51485..5ee92747196 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -346,6 +346,7 @@ class SpeechManager(object): try: data = yield from self.hass.loop.run_in_executor(None, load_speech) except OSError: + del self.file_cache[key] raise HomeAssistantError("Can't read {}".format(voice_file)) self._async_store_to_memcache(key, filename, data) From 6b0a6b87defd2b80c40b2be488cc6cc0664a2830 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 20 Jan 2017 20:23:20 -0800 Subject: [PATCH 082/191] Use is_screensaver --- homeassistant/components/media_player/roku.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 728777e5e9e..ff4ddebbadf 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -17,8 +17,8 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/bah2830/python-roku/archive/3.1.2.zip' - '#roku==3.1.2'] + 'https://github.com/bah2830/python-roku/archive/3.1.3.zip' + '#roku==3.1.3'] KNOWN_HOSTS = [] DEFAULT_PORT = 8060 @@ -114,8 +114,8 @@ class RokuDevice(MediaPlayerDevice): if self.current_app is None: return STATE_UNKNOWN - idle_list = ["Power Saver", "Screensaver", "screensaver"] - if any(idle_type in self.current_app.name for idle_type in idle_list): + if (self.current_app.name == "Power Saver" or + self.current_app.is_screensaver): return STATE_IDLE elif self.current_app.name == "Roku": return STATE_HOME From 26f6a9ee20d420fd829d82cffe8a260a05233fd2 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 20 Jan 2017 20:24:00 -0800 Subject: [PATCH 083/191] Bump requirements --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index d13833efe18..ad7c5b75b81 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -202,7 +202,7 @@ https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 https://github.com/aparraga/braviarc/archive/0.3.6.zip#braviarc==0.3.6 # homeassistant.components.media_player.roku -https://github.com/bah2830/python-roku/archive/3.1.2.zip#roku==3.1.2 +https://github.com/bah2830/python-roku/archive/3.1.3.zip#roku==3.1.3 # homeassistant.components.modbus https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 From 58b698400e8c23c135d585f53f6e2ac9cc516c93 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 20 Jan 2017 21:28:29 -0800 Subject: [PATCH 084/191] Set Roku name to the device name instead of the serial number (#5475) --- homeassistant/components/media_player/roku.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 5a4e993aee5..35aa07a5a4b 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -69,10 +69,10 @@ class RokuDevice(MediaPlayerDevice): from roku import Roku self.roku = Roku(host) - self.roku_name = None self.ip_address = host self.channels = [] self.current_app = None + self.device_info = {} self.update() @@ -81,7 +81,7 @@ class RokuDevice(MediaPlayerDevice): import requests.exceptions try: - self.roku_name = "roku_" + self.roku.device_info.sernum + self.device_info = self.roku.device_info self.ip_address = self.roku.host self.channels = self.get_source_list() @@ -106,7 +106,7 @@ class RokuDevice(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - return self.roku_name + return self.device_info.userdevicename @property def state(self): From b2203f7f4153c7d32b3f6b2201b78bbe9a242e1f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 21 Jan 2017 06:56:22 +0100 Subject: [PATCH 085/191] [ffmpeg] Use new 1.0 version / migrate all asyncio (#5464) * [ffmpeg] Use new 1.0 version / migrate all asyncio * fix lint * fix import * Add new service to binary_sensors * fix lint --- .../components/binary_sensor/ffmpeg.py | 174 ++++++++++++------ .../components/binary_sensor/services.yaml | 18 +- homeassistant/components/camera/ffmpeg.py | 20 +- homeassistant/components/ffmpeg.py | 98 +++++----- requirements_all.txt | 2 +- 5 files changed, 190 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index 818a6b5b387..9da14048705 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -4,8 +4,9 @@ Provides a binary sensor which is a collection of ffmpeg tools. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ffmpeg/ """ +import asyncio import logging -from os import path +import os import voluptuous as vol @@ -13,17 +14,22 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN) from homeassistant.components.ffmpeg import ( - get_binary, run_test, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) + DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) from homeassistant.config import load_yaml_config_file -from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME, - ATTR_ENTITY_ID) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_NAME, + ATTR_ENTITY_ID) DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) +SERVICE_START = 'ffmpeg_start' +SERVICE_STOP = 'ffmpeg_stop' SERVICE_RESTART = 'ffmpeg_restart' +DATA_FFMPEG_DEVICE = 'ffmpeg_binary_sensor' + FFMPEG_SENSOR_NOISE = 'noise' FFMPEG_SENSOR_MOTION = 'motion' @@ -32,6 +38,7 @@ MAP_FFMPEG_BIN = [ FFMPEG_SENSOR_MOTION ] +CONF_INITIAL_STATE = 'initial_state' CONF_TOOL = 'tool' CONF_PEAK = 'peak' CONF_DURATION = 'duration' @@ -41,10 +48,12 @@ CONF_REPEAT = 'repeat' CONF_REPEAT_TIME = 'repeat_time' DEFAULT_NAME = 'FFmpeg' +DEFAULT_INIT_STATE = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN), vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, vol.Optional(CONF_OUTPUT): cv.string, @@ -61,7 +70,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=0)), }) -SERVICE_RESTART_SCHEMA = vol.Schema({ +SERVICE_FFMPEG_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -72,86 +81,125 @@ def restart(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_RESTART, data) -# list of all ffmpeg sensors -DEVICES = [] - - -def setup_platform(hass, config, add_entities, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Create the binary sensor.""" from haffmpeg import SensorNoise, SensorMotion # check source - if not run_test(hass, config.get(CONF_INPUT)): + if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return # generate sensor object if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE: - entity = FFmpegNoise(SensorNoise, config) + entity = FFmpegNoise(hass, SensorNoise, config) else: - entity = FFmpegMotion(SensorMotion, config) + entity = FFmpegMotion(hass, SensorMotion, config) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, entity.shutdown_ffmpeg) + @asyncio.coroutine + def async_shutdown(event): + """Stop ffmpeg.""" + yield from entity.async_shutdown_ffmpeg() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_shutdown) + + # start on startup + if config.get(CONF_INITIAL_STATE): + @asyncio.coroutine + def async_start(event): + """Start ffmpeg.""" + yield from entity.async_start_ffmpeg() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_start) # add to system - add_entities([entity]) - DEVICES.append(entity) + yield from async_add_devices([entity]) # exists service? if hass.services.has_service(DOMAIN, SERVICE_RESTART): + hass.data[DATA_FFMPEG_DEVICE].append(entity) return + hass.data[DATA_FFMPEG_DEVICE] = [entity] - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, + os.path.join(os.path.dirname(__file__), 'services.yaml')) # register service - def _service_handle_restart(service): + @asyncio.coroutine + def async_service_handle(service): """Handle service binary_sensor.ffmpeg_restart.""" entity_ids = service.data.get('entity_id') if entity_ids: - _devices = [device for device in DEVICES + _devices = [device for device in hass.data[DATA_FFMPEG_DEVICE] if device.entity_id in entity_ids] else: - _devices = DEVICES + _devices = hass.data[DATA_FFMPEG_DEVICE] + tasks = [] for device in _devices: - device.restart_ffmpeg() + if service.service == SERVICE_START: + tasks.append(device.async_start_ffmpeg()) + elif service.service == SERVICE_STOP: + tasks.append(device.async_shutdown_ffmpeg()) + else: + tasks.append(device.async_restart_ffmpeg()) - hass.services.register(DOMAIN, SERVICE_RESTART, - _service_handle_restart, - descriptions.get(SERVICE_RESTART), - schema=SERVICE_RESTART_SCHEMA) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + hass.services.async_register( + DOMAIN, SERVICE_START, async_service_handle, + descriptions.get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_STOP, async_service_handle, + descriptions.get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_RESTART, async_service_handle, + descriptions.get(SERVICE_RESTART), schema=SERVICE_FFMPEG_SCHEMA) class FFmpegBinarySensor(BinarySensorDevice): """A binary sensor which use ffmpeg for noise detection.""" - def __init__(self, ffobj, config): + def __init__(self, hass, ffobj, config): """Constructor for binary sensor noise detection.""" + self._manager = hass.data[DATA_FFMPEG] self._state = False self._config = config self._name = config.get(CONF_NAME) - self._ffmpeg = ffobj(get_binary(), self._callback) + self._ffmpeg = ffobj( + self._manager.binary, hass.loop, self._async_callback) - self._start_ffmpeg(config) - - def _callback(self, state): + def _async_callback(self, state): """HA-FFmpeg callback for noise detection.""" self._state = state - self.schedule_update_ha_state() + self.hass.async_add_job(self.async_update_ha_state()) - def _start_ffmpeg(self, config): - """Start a FFmpeg instance.""" - raise NotImplementedError + def async_start_ffmpeg(self): + """Start a FFmpeg instance. - def shutdown_ffmpeg(self, event): - """For STOP event to shutdown ffmpeg.""" - self._ffmpeg.close() + This method must be run in the event loop and returns a coroutine. + """ + raise NotImplementedError() - def restart_ffmpeg(self): - """Restart ffmpeg with new config.""" - self._ffmpeg.close() - self._start_ffmpeg(self._config) + def async_shutdown_ffmpeg(self): + """For STOP event to shutdown ffmpeg. + + This method must be run in the event loop and returns a coroutine. + """ + return self._ffmpeg.close() + + @asyncio.coroutine + def async_restart_ffmpeg(self): + """Restart processing.""" + yield from self.async_shutdown_ffmpeg() + yield from self.async_start_ffmpeg() @property def is_on(self): @@ -177,20 +225,23 @@ class FFmpegBinarySensor(BinarySensorDevice): class FFmpegNoise(FFmpegBinarySensor): """A binary sensor which use ffmpeg for noise detection.""" - def _start_ffmpeg(self, config): - """Start a FFmpeg instance.""" + def async_start_ffmpeg(self): + """Start a FFmpeg instance. + + This method must be run in the event loop and returns a coroutine. + """ # init config self._ffmpeg.set_options( - time_duration=config.get(CONF_DURATION), - time_reset=config.get(CONF_RESET), - peak=config.get(CONF_PEAK), + time_duration=self._config.get(CONF_DURATION), + time_reset=self._config.get(CONF_RESET), + peak=self._config.get(CONF_PEAK), ) # run - self._ffmpeg.open_sensor( - input_source=config.get(CONF_INPUT), - output_dest=config.get(CONF_OUTPUT), - extra_cmd=config.get(CONF_EXTRA_ARGUMENTS), + return self._ffmpeg.open_sensor( + input_source=self._config.get(CONF_INPUT), + output_dest=self._config.get(CONF_OUTPUT), + extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) @property @@ -202,20 +253,23 @@ class FFmpegNoise(FFmpegBinarySensor): class FFmpegMotion(FFmpegBinarySensor): """A binary sensor which use ffmpeg for noise detection.""" - def _start_ffmpeg(self, config): - """Start a FFmpeg instance.""" + def async_start_ffmpeg(self): + """Start a FFmpeg instance. + + This method must be run in the event loop and returns a coroutine. + """ # init config self._ffmpeg.set_options( - time_reset=config.get(CONF_RESET), - time_repeat=config.get(CONF_REPEAT_TIME), - repeat=config.get(CONF_REPEAT), - changes=config.get(CONF_CHANGES), + time_reset=self._config.get(CONF_RESET), + time_repeat=self._config.get(CONF_REPEAT_TIME), + repeat=self._config.get(CONF_REPEAT), + changes=self._config.get(CONF_CHANGES), ) # run - self._ffmpeg.open_sensor( - input_source=config.get(CONF_INPUT), - extra_cmd=config.get(CONF_EXTRA_ARGUMENTS), + return self._ffmpeg.open_sensor( + input_source=self._config.get(CONF_INPUT), + extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) @property diff --git a/homeassistant/components/binary_sensor/services.yaml b/homeassistant/components/binary_sensor/services.yaml index 9be9915e268..a1ac8cf8b5d 100644 --- a/homeassistant/components/binary_sensor/services.yaml +++ b/homeassistant/components/binary_sensor/services.yaml @@ -1,7 +1,23 @@ # Describes the format for available binary_sensor services +ffmpeg_start: + description: Send a start command to a ffmpeg based sensor. + + fields: + entity_id: + description: Name(s) of entites that will start. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' + +ffmpeg_stop: + description: Send a stop command to a ffmpeg based sensor. + + fields: + entity_id: + description: Name(s) of entites that will stop. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' + ffmpeg_restart: - description: Send a restart command to a ffmpeg based sensor (party mode). + description: Send a restart command to a ffmpeg based sensor. fields: entity_id: diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 0b8d60ab7f5..6b00ae240ed 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -12,10 +12,9 @@ from aiohttp import web from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( - async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) + DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME -from homeassistant.util.async import run_coroutine_threadsafe DEPENDENCIES = ['ffmpeg'] @@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" - if not async_run_test(hass, config.get(CONF_INPUT)): + if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return yield from async_add_devices([FFmpegCamera(hass, config)]) @@ -44,20 +43,17 @@ class FFmpegCamera(Camera): def __init__(self, hass, config): """Initialize a FFmpeg camera.""" super().__init__() + + self._manager = hass.data[DATA_FFMPEG] self._name = config.get(CONF_NAME) self._input = config.get(CONF_INPUT) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) - def camera_image(self): - """Return bytes of camera image.""" - return run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop).result() - @asyncio.coroutine def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg import ImageSingleAsync, IMAGE_JPEG - ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop) + from haffmpeg import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) image = yield from ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, @@ -67,9 +63,9 @@ class FFmpegCamera(Camera): @asyncio.coroutine def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg import CameraMjpegAsync + from haffmpeg import CameraMjpeg - stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop) + stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) yield from stream.open_camera( self._input, extra_cmd=self._extra_arguments) diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index f345153e666..56e1cb8c95d 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -10,13 +10,14 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==0.15"] +REQUIREMENTS = ["ha-ffmpeg==1.0"] _LOGGER = logging.getLogger(__name__) +DATA_FFMPEG = 'ffmpeg' + CONF_INPUT = 'input' CONF_FFMPEG_BIN = 'ffmpeg_bin' CONF_EXTRA_ARGUMENTS = 'extra_arguments' @@ -34,53 +35,54 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -FFMPEG_CONFIG = { - CONF_FFMPEG_BIN: DEFAULT_BINARY, - CONF_RUN_TEST: DEFAULT_RUN_TEST, -} -FFMPEG_TEST_CACHE = {} - - -def setup(hass, config): - """Setup the FFmpeg component.""" - if DOMAIN in config: - FFMPEG_CONFIG.update(config.get(DOMAIN)) - return True - - -def get_binary(): - """Return ffmpeg binary from config. - - Async friendly. - """ - return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) - - -def run_test(hass, input_source): - """Run test on this input. TRUE is deactivate or run correct.""" - return run_coroutine_threadsafe( - async_run_test(hass, input_source), hass.loop).result() - - @asyncio.coroutine -def async_run_test(hass, input_source): - """Run test on this input. TRUE is deactivate or run correct. +def async_setup(hass, config): + """Setup the FFmpeg component.""" + conf = config.get(DOMAIN, {}) - This method must be run in the event loop. - """ - from haffmpeg import TestAsync + hass.data[DATA_FFMPEG] = FFmpegManager( + hass, + conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY), + conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST) + ) - if FFMPEG_CONFIG.get(CONF_RUN_TEST): - # if in cache - if input_source in FFMPEG_TEST_CACHE: - return FFMPEG_TEST_CACHE[input_source] - - # run test - ffmpeg_test = TestAsync(get_binary(), loop=hass.loop) - success = yield from ffmpeg_test.run_test(input_source) - if not success: - _LOGGER.error("FFmpeg '%s' test fails!", input_source) - FFMPEG_TEST_CACHE[input_source] = False - return False - FFMPEG_TEST_CACHE[input_source] = True return True + + +class FFmpegManager(object): + """Helper for ha-ffmpeg.""" + + def __init__(self, hass, ffmpeg_bin, run_test): + """Initialize helper.""" + self.hass = hass + self._cache = {} + self._bin = ffmpeg_bin + self._run_test = run_test + + @property + def binary(self): + """Return ffmpeg binary from config.""" + return self._bin + + @asyncio.coroutine + def async_run_test(self, input_source): + """Run test on this input. TRUE is deactivate or run correct. + + This method must be run in the event loop. + """ + from haffmpeg import Test + + if self._run_test: + # if in cache + if input_source in self._cache: + return self._cache[input_source] + + # run test + ffmpeg_test = Test(self.binary, loop=self.hass.loop) + success = yield from ffmpeg_test.run_test(input_source) + if not success: + _LOGGER.error("FFmpeg '%s' test fails!", input_source) + self._cache[input_source] = False + return False + self._cache[input_source] = True + return True diff --git a/requirements_all.txt b/requirements_all.txt index df8dc7e77b9..26300090067 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ googlemaps==2.4.4 gps3==0.33.3 # homeassistant.components.ffmpeg -ha-ffmpeg==0.15 +ha-ffmpeg==1.0 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.1 From 14309401d02e9c5f3f4916ad68986887a1c64846 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Jan 2017 22:10:44 -0800 Subject: [PATCH 086/191] Update frontend --- homeassistant/components/frontend/version.py | 6 +++--- .../components/frontend/www_static/core.js | 8 ++++---- .../components/frontend/www_static/core.js.gz | Bin 32917 -> 33554 bytes .../frontend/www_static/frontend.html | 4 ++-- .../frontend/www_static/frontend.html.gz | Bin 131597 -> 132223 bytes .../www_static/home-assistant-polymer | 2 +- .../panels/ha-panel-dev-service.html | 2 +- .../panels/ha-panel-dev-service.html.gz | Bin 17837 -> 17910 bytes .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 2324 -> 2326 bytes 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 3af14628008..d1a4c4e8f93 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": "22d39af274e1d824ca1302e10971f2d8", - "frontend.html": "61e57194179b27563a05282b58dd4f47", + "core.js": "90c16d2f2c5d52203e2fd5fa2b1ae19c", + "frontend.html": "c8e670c6c9f7c0ea3b971b92ba9013db", "mdi.html": "5bb2f1717206bad0d187c2633062c575", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5", "panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6", - "panels/ha-panel-dev-service.html": "e32bcd3afdf485417a3e20b4fc760776", + "panels/ha-panel-dev-service.html": "1d223225c1c75083738033895ea3e4b5", "panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0", "panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b", "panels/ha-panel-history.html": "7baeb4bd7d9ce0def4f95eab6f10812e", diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index e3134a1ea79..3494529ef9b 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 xe({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function r(){return Ve.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 Fe({authToken:n,host:r})}function u(){return Ge.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 Xe.getInitialState()}function h(t){return{type:"auth",api_password:t}}function l(){return{type:"get_states"}}function p(){return{type:"get_config"}}function _(){return{type:"get_services"}}function d(){return{type:"get_panels"}}function v(t,e,n){var r={type:"call_service",domain:t,service:e};return n&&(r.service_data=n),r}function y(t){var e={type:"subscribe_events"};return t&&(e.event_type=t),e}function m(t){return{type:"unsubscribe_events",subscription:t}}function g(){return{type:"ping"}}function S(t,e){return{type:"result",success:!1,error:{code:t,message:e}}}function b(t){return t.result}function E(t,e){var n=new tn(t,e);return n.connect()}function I(t,e,n,r){void 0===r&&(r=null);var i=t.evaluate(Mo.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 O(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?sn({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||sn;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function mt(t,e){var n=e.date;return n.toISOString()}function gt(){return Qr.getInitialState()}function St(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,$r({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],$r(e.map(In.fromJSON)))}))}))}function bt(){return ti.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,ii(e.map(In.fromJSON)))}))}))}function It(){return oi.getInitialState()}function Ot(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(si,r)}))}function wt(){return ci.getInitialState()}function Tt(t,e){t.dispatch(Wr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function At(t,e){void 0===e&&(e=null),t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),on(t,"GET",n).then((function(e){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function Dt(t,e){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_START,{date:e}),on(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function Ct(t){var e=t.evaluate(li);return Dt(t,e)}function zt(t){t.registerStores({currentEntityHistoryDate:Qr,entityHistory:ti,isLoadingEntityHistory:ni,recentEntityHistory:oi,recentEntityHistoryUpdated:ci})}function Rt(t){t.registerStores({moreInfoEntityId:Yr})}function Mt(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 Yt(t){var e=fo[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),fo[t.hassId]=!1)}function Jt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),Yt(t);var r=t.evaluate(Mo.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=Bt((function(){return e.ping()}),so);r(),e.socket.addEventListener("message",r),fo[t.hassId]={conn:e,scheduleHealthCheck:r},co.forEach((function(n){return e.subscribeEvents(ao.bind(null,t),n)})),t.batch((function(){t.dispatch(Ye.STREAM_START),n&&io.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(Ye.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(Ye.STREAM_START),io.fetchAll(t)}))}))}))}function Wt(t){t.registerStores({streamStatus:Xe})}function Xt(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(Ue.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),io.fetchAll(t).then((function(){t.dispatch(Ue.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),vo.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=go),t.dispatch(Ue.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Qt(t){t.dispatch(Ue.LOG_OUT,{})}function Zt(t){t.registerStores({authAttempt:Ve,authCurrent:Ge,rememberAuth:Be})}function $t(){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 te(){var t=new Uo({debug:!1});return t.hassId=Ho++,t}function ee(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 ne(t,e){return xo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function re(t){return on(t,"GET","error_log")}function ie(t,e){var n=e.date;return n.toISOString()}function oe(){return Jo.getInitialState()}function ue(t,e){var n=e.date,r=e.entries;return t.set(n,eu(r.map($o.fromJSON)))}function ae(){return nu.getInitialState()}function se(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function ce(){return ou.getInitialState()}function fe(t,e){t.dispatch(Bo.LOGBOOK_DATE_SELECTED,{date:e})}function he(t,e){t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),on(t,"GET","logbook/"+e).then((function(n){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function le(t){return!t||(new Date).getTime()-t>su}function pe(t){t.registerStores({currentLogbookDate:Jo,isLoadingLogbookEntries:Xo,logbookEntries:nu,logbookEntriesUpdated:ou})}function _e(t){return t.set("active",!0)}function de(t){return t.set("active",!1)}function ve(){return Su.getInitialState()}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){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",on(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(yu.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),Vn.createNotification(t,n),!1}))}function me(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 on(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(yu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Vn.createNotification(t,n),!1}))}function ge(t){t.registerStores({pushNotifications:Su})}function Se(t,e){return on(t,"POST","template",{template:e})}function be(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 Ie(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 Oe(){return ku.getInitialState()}function we(){return ku.getInitialState()}function Te(){return ku.getInitialState()}function Ae(t){return Pu[t.hassId]}function De(t){var e=Ae(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(ju.VOICE_TRANSMITTING,{finalTranscript:n}),tr.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(ju.VOICE_DONE)}),(function(){t.dispatch(ju.VOICE_ERROR)}))}}function Ce(t){var e=Ae(t);e&&(e.recognition.stop(),Pu[t.hassId]=!1)}function ze(t){De(t),Ce(t)}function Re(t){var e=ze.bind(null,t);e();var n=new webkitSpeechRecognition;Pu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(ju.VOICE_START)},n.onerror=function(){return t.dispatch(ju.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ae(t);if(n){for(var r="",i="",o=e.resultIndex;o=n)}function c(t,e){return h(t,e,0)}function f(t,e){return h(t,e,e)}function h(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function l(t){return v(t)?t:C(t)}function p(t){return y(t)?t:z(t)}function _(t){return m(t)?t:R(t)}function d(t){return v(t)&&!g(t)?t:M(t)}function v(t){return!(!t||!t[dn])}function y(t){return!(!t||!t[vn])}function m(t){return!(!t||!t[yn])}function g(t){return y(t)||m(t)}function S(t){return!(!t||!t[mn])}function b(t){this.next=t}function E(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&&(En&&t[En]||t[In]);if("function"==typeof e)return e}function D(t){return t&&"number"==typeof t.length}function C(t){return null===t||void 0===t?U():v(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?U().toKeyedSeq():v(t)?y(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?U():v(t)?y(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?U():v(t)?y(t)?t.entrySeq():t:x(t)).toSetSeq()}function j(t){this._array=t,this.size=t.length}function L(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 P(t){return!(!t||!t[wn])}function U(){return Tn||(Tn=new j([]))}function H(t){var e=Array.isArray(t)?new j(t).fromEntrySeq():w(t)?new k(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():"object"==typeof t?new L(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 L(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return D(t)?new j(t):w(t)?new k(t):O(t)?new N(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 b(function(){var t=i[n?o-u:u];return u++>o?I():E(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(){throw TypeError("Abstract")}function B(){}function Y(){}function J(){}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){return e?Q(e,t,"",{"":t}):Z(t)}function Q(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map((function(n,r){return Q(t,n,r,e)}))):$(e)?t.call(r,n,z(e).map((function(n,r){return Q(t,n,r,e)}))):e}function Z(t){return Array.isArray(t)?R(t).map(Z).toList():$(t)?z(t).map(Z).toMap():t}function $(t){return t&&(t.constructor===Object||void 0===t.constructor)}function tt(t){return t>>>1&1073741824|3221225471&t}function et(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){var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return tt(n)}return"string"===e?t.length>Ln?nt(t):rt(t):"function"==typeof t.hashCode?t.hashCode():it(t)}function nt(t){var e=Pn[t];return void 0===e&&(e=rt(t),kn===Nn&&(kn=0,Pn={}),kn++,Pn[t]=e),e}function rt(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 ut(t,e){if(!t)throw new Error(e)}function at(t){ut(t!==1/0,"Cannot perform this action with an infinite size.")}function st(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ct(t){this._iter=t,this.size=t.size}function ft(t){this._iter=t,this.size=t.size}function ht(t){this._iter=t,this.size=t.size}function lt(t){var e=jt(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=Lt,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 b(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===Sn?gn:Sn,n)},e}function pt(t,e,n){var r=jt(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,ln);return o===ln?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 b(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return E(r,a,e.call(n,u[1],a,t),i)})},r}function _t(t,e){var n=jt(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=lt(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=Lt,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 dt(t,e,n,r){var i=jt(t);return r&&(i.has=function(r){var i=t.get(r,ln);return i!==ln&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,ln);return o!==ln&&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 b(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 E(i,r?c:a++,f,o)}})},i}function vt(t,e,n){var r=Pt().asMutable();return t.__iterate((function(i,o){r.update(e.call(n,i,o,t),0,(function(t){return t+1}))})),r.asImmutable()}function yt(t,e,n){var r=y(t),i=(S(t)?Ie():Pt()).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=Mt(t);return i.map((function(e){return Ct(t,o(e))}))}function mt(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n|=0),s(e,n,i))return t;var o=c(e,i),a=f(n,i);if(o!==o||a!==a)return mt(t.toSeq().cacheResult(),e,n,r);var h,l=a-o;l===l&&(h=l<0?0:l);var p=jt(t);return p.size=0===h?h:t.size&&h||void 0,!r&&P(t)&&h>=0&&(p.get=function(e,n){return e=u(this,e),e>=0&&eh)return I();var t=i.next();return r||e===Sn?t:e===gn?E(e,a-1,void 0,t):E(e,a-1,t.value[1],t)})},p}function gt(t,e,n){var r=jt(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 b(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:E(r,s,c,t):(a=!1,I())})},r}function St(t,e,n,r){var i=jt(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 b(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===Sn?t:i===gn?E(i,c++,void 0,t):E(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:E(i,o,f,t)})},i}function bt(t,e){var n=y(t),r=[t].concat(e).map((function(t){return v(t)?n&&(t=p(t)):t=n?H(t):x(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===r.length)return t;if(1===r.length){var i=r[0];if(i===t||n&&y(i)||m(t)&&m(i))return i}var o=new j(r);return n?o=o.toKeyedSeq():m(t)||(o=o.toSetSeq()),o=o.flatten(!0),o.size=r.reduce((function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}}),0),o}function Et(t,e,n){var r=jt(t);return r.__iterateUncached=function(r,i){function o(t,s){var c=this;t.__iterate((function(t,i){return(!e||s0}function Dt(t,e,n){var r=jt(t);return r.size=new j(n).map((function(t){return t.size})).min(),r.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(Sn,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},r.__iteratorUncached=function(t,r){var i=n.map((function(t){return t=l(t),T(r?t.reverse():t)})),o=0,u=!1; -return new b(function(){var n;return u||(n=i.map((function(t){return t.next()})),u=n.some((function(t){return t.done}))),u?I():E(t,o++,e.apply(null,n.map((function(t){return t.value}))))})},r}function Ct(t,e){return P(t)?e:t.constructor(e)}function zt(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Rt(t){return at(t.size),o(t)}function Mt(t){return y(t)?p:m(t)?_:d}function jt(t){return Object.create((y(t)?z:m(t)?R:M).prototype)}function Lt(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):C.prototype.cacheResult.call(this)}function Nt(t,e){return t>e?1:t>>n)&hn,a=(0===n?r:r>>>n)&hn,s=u===a?[Zt(t,e,n+cn,r,i)]:(o=new Ft(e,r,i),u>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new Vt(t,o+1,u)}function ne(t,e,n){for(var r=[],i=0;i>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function ae(t,e,n,r){var o=r?t:i(t);return o[e]=n,o}function se(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&&ro?0:o-n,c=u-n;return c>fn&&(c=fn),function(){if(i===c)return Yn;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>fn&&(f=fn),function(){for(;;){if(a){var t=a();if(t!==Yn)return t;a=null}if(c===f)return Yn;var o=e?--f:c++;a=n(s&&s[o],r-cn,i+(o<=t.size||n<0)return t.withMutations((function(t){n<0?Se(t,n).set(0,r):Se(t,0,n+1).set(n,r)}));n+=t._origin;var i=t._tail,o=t._root,a=e(_n);return n>=Ee(t._capacity)?i=ye(i,t.__ownerID,0,n,r,a):o=ye(o,t.__ownerID,t._level,n,r,a),a.value?t.__ownerID?(t._root=o,t._tail=i,t.__hash=void 0,t.__altered=!0,t):_e(t._origin,t._capacity,t._level,o,i):t}function ye(t,e,r,i,o,u){var a=i>>>r&hn,s=t&&a0){var f=t&&t.array[a],h=ye(f,e,r-cn,i,o,u);return h===f?t:(c=me(t,e),c.array[a]=h,c)}return s&&t.array[a]===o?t:(n(u),c=me(t,e),void 0===o&&a===c.array.length-1?c.array.pop():c.array[a]=o,c)}function me(t,e){return e&&t&&e===t.ownerID?t:new le(t?t.array.slice():[],e)}function ge(t,e){if(e>=Ee(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&hn],r-=cn;return n}}function Se(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var i=t.__ownerID||new r,o=t._origin,u=t._capacity,a=o+e,s=void 0===n?u:n<0?u+n:o+n;if(a===o&&s===u)return t;if(a>=s)return t.clear();for(var c=t._level,f=t._root,h=0;a+h<0;)f=new le(f&&f.array.length?[void 0,f]:[],i),c+=cn,h+=1<=1<l?new le([],i):_;if(_&&p>l&&acn;y-=cn){var m=l>>>y&hn;v=v.array[m]=me(v.array[m],i)}v.array[l>>>cn&hn]=_}if(s=p)a-=p,s-=p,c=cn,f=null,d=d&&d.removeBefore(i,0,a);else if(a>o||p>>c&hn;if(g!==p>>>c&hn)break;g&&(h+=(1<o&&(f=f.removeBefore(i,c,a-h)),f&&pi&&(i=a.size),v(u)||(a=a.map((function(t){return X(t)}))),r.push(a)}return i>t.size&&(t=t.setSize(i)),ie(t,e,r)}function Ee(t){return t>>cn<=fn&&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):we(r,i)}function De(t){return null===t||void 0===t?Re():Ce(t)?t:Re().unshiftAll(t)}function Ce(t){return!(!t||!t[Wn])}function ze(t,e,n,r){var i=Object.create(Xn);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Re(){return Qn||(Qn=ze(0))}function Me(t){return null===t||void 0===t?ke():je(t)&&!S(t)?t:ke().withMutations((function(e){var n=d(t);at(n.size),n.forEach((function(t){return e.add(t)}))}))}function je(t){return!(!t||!t[Zn])}function Le(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function Ne(t,e){var n=Object.create($n);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function ke(){return tr||(tr=Ne(Jt()))}function Pe(t){return null===t||void 0===t?xe():Ue(t)?t:xe().withMutations((function(e){var n=d(t);at(n.size),n.forEach((function(t){return e.add(t)}))}))}function Ue(t){return je(t)&&S(t)}function He(t,e){var n=Object.create(er);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function xe(){return nr||(nr=He(Te()))}function Ve(t,e){var n,r=function(o){if(o instanceof r)return o;if(!(this instanceof r))return new r(o);if(!n){n=!0;var u=Object.keys(t);Ge(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=Pt(o)},i=r.prototype=Object.create(rr);return i.constructor=r,r}function qe(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Fe(t){return t._name||t.constructor.name||"Record"}function Ge(t,e){try{e.forEach(Ke.bind(void 0,t))}catch(t){}}function Ke(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){ut(this.__ownerID,"Cannot set on an immutable record."),this.set(e,t)}})}function Be(t,e){if(t===e)return!0;if(!v(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||y(t)!==y(e)||m(t)!==m(e)||S(t)!==S(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!g(t);if(S(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 o=t;t=e,e=o}var u=!0,a=e.__iterate((function(e,r){if(n?!t.has(e):i?!W(e,t.get(r,ln)):!W(t.get(r,ln),e))return u=!1,!1}));return u&&t.size===a}function Ye(t,e,n){if(!(this instanceof Ye))return new Ye(t,e,n);if(ut(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),ee?-1:0}function rn(t){if(t.size===1/0)return 0;var e=S(t),n=y(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+un(et(t),et(e))|0}:function(t,e){r=r+un(et(t),et(e))|0}:e?function(t){r=31*r+et(t)|0}:function(t){r=r+et(t)|0});return on(i,r)}function on(t,e){return e=Dn(e,3432918353),e=Dn(e<<15|e>>>-15,461845907),e=Dn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Dn(e^e>>>16,2246822507),e=Dn(e^e>>>13,3266489909),e=tt(e^e>>>16)}function un(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var an=Array.prototype.slice,sn="delete",cn=5,fn=1<r?I():E(t,i,n[e?r-i++:i++])})},t(L,z),L.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},L.prototype.has=function(t){return this._object.hasOwnProperty(t)},L.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},L.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new b(function(){var u=r[e?i-o:o];return o++>i?I():E(t,u,n[u])})},L.prototype[mn]=!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 b(I);var i=0;return new b(function(){var e=r.next();return e.done?e:E(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 E(t,i,r[i++])})};var Tn;t(K,l),t(B,K),t(Y,K),t(J,K),K.Keyed=B,K.Indexed=Y,K.Set=J;var An,Dn="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},Cn=Object.isExtensible,zn=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),Rn="function"==typeof WeakMap;Rn&&(An=new WeakMap);var Mn=0,jn="__immutablehash__";"function"==typeof Symbol&&(jn=Symbol(jn));var Ln=16,Nn=255,kn=0,Pn={};t(st,z),st.prototype.get=function(t,e){return this._iter.get(t,e)},st.prototype.has=function(t){return this._iter.has(t)},st.prototype.valueSeq=function(){return this._iter.valueSeq()},st.prototype.reverse=function(){var t=this,e=_t(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},st.prototype.map=function(t,e){var n=this,r=pt(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},st.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?Rt(this):0,function(i){return t(i,e?--n:n++,r)}),e)},st.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(Sn,e),r=e?Rt(this):0;return new b(function(){var i=n.next();return i.done?i:E(t,e?--r:r++,i.value,i)})},st.prototype[mn]=!0,t(ct,R),ct.prototype.includes=function(t){return this._iter.includes(t)},ct.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate((function(e){return t(e,r++,n)}),e)},ct.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Sn,e),r=0;return new b(function(){var e=n.next();return e.done?e:E(t,r++,e.value,e)})},t(ft,M),ft.prototype.has=function(t){return this._iter.includes(t)},ft.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){return t(e,e,n)}),e)},ft.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Sn,e);return new b(function(){var e=n.next();return e.done?e:E(t,e.value,e.value,e)})},t(ht,z),ht.prototype.entrySeq=function(){return this._iter.toSeq()},ht.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate((function(e){if(e){zt(e);var r=v(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}}),e)},ht.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Sn,e);return new b(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){zt(r);var i=v(r);return E(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ct.prototype.cacheResult=st.prototype.cacheResult=ft.prototype.cacheResult=ht.prototype.cacheResult=Lt,t(Pt,B),Pt.prototype.toString=function(){return this.__toString("Map {","}")},Pt.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},Pt.prototype.set=function(t,e){return Wt(this,t,e)},Pt.prototype.setIn=function(t,e){return this.updateIn(t,ln,(function(){return e}))},Pt.prototype.remove=function(t){return Wt(this,t,ln)},Pt.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return ln}))},Pt.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},Pt.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=oe(this,kt(t),e,n);return r===ln?void 0:r},Pt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Jt()},Pt.prototype.merge=function(){return ne(this,void 0,arguments)},Pt.prototype.mergeWith=function(t){var e=an.call(arguments,1);return ne(this,t,e)},Pt.prototype.mergeIn=function(t){var e=an.call(arguments,1);return this.updateIn(t,Jt(),(function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]}))},Pt.prototype.mergeDeep=function(){return ne(this,re(void 0),arguments)},Pt.prototype.mergeDeepWith=function(t){var e=an.call(arguments,1);return ne(this,re(t),e)},Pt.prototype.mergeDeepIn=function(t){var e=an.call(arguments,1);return this.updateIn(t,Jt(),(function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]}))},Pt.prototype.sort=function(t){return Ie(wt(this,t))},Pt.prototype.sortBy=function(t,e){return Ie(wt(this,e,t))},Pt.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},Pt.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new r)},Pt.prototype.asImmutable=function(){return this.__ensureOwner()},Pt.prototype.wasAltered=function(){return this.__altered},Pt.prototype.__iterator=function(t,e){return new Gt(this,t,e)},Pt.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},Pt.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Yt(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Pt.isMap=Ut;var Un="@@__IMMUTABLE_MAP__@@",Hn=Pt.prototype;Hn[Un]=!0,Hn[sn]=Hn.remove,Hn.removeIn=Hn.deleteIn,Ht.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Vn)return $t(t,f,o,u);var _=t&&t===this.ownerID,d=_?f:i(f);return p?c?h===l-1?d.pop():d[h]=d.pop():d[h]=[o,u]:d.push([o,u]),_?(this.entries=d,this):new Ht(t,d)}},xt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=1<<((0===t?e:e>>>t)&hn),o=this.bitmap;return 0===(o&i)?r:this.nodes[ue(o&i-1)].get(t+cn,e,n,r)},xt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&hn,s=1<=qn)return ee(t,l,c,a,_);if(f&&!_&&2===l.length&&Qt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&Qt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?ae(l,h,_,d):ce(l,h,d):se(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new xt(t,v,y)},Vt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=(0===t?e:e>>>t)&hn,o=this.nodes[i];return o?o.get(t+cn,e,n,r):r},Vt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&hn,s=i===ln,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Xt(f,t,e+cn,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&hn;if(r>=this.array.length)return new le([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-cn,n),i===u&&o)return this}if(o&&!i)return this;var a=me(this,t);if(!o)for(var s=0;s>>e&hn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-cn,n),i===o&&r===this.array.length-1)return this}var u=me(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Bn,Yn={};t(Ie,Pt),Ie.of=function(){return this(arguments)},Ie.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Ie.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Ie.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):Te()},Ie.prototype.set=function(t,e){return Ae(this,t,e)},Ie.prototype.remove=function(t){return Ae(this,t,ln)},Ie.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Ie.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},Ie.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Ie.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?we(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Ie.isOrderedMap=Oe,Ie.prototype[mn]=!0,Ie.prototype[sn]=Ie.prototype.remove;var Jn;t(De,Y),De.of=function(){return this(arguments)},De.prototype.toString=function(){return this.__toString("Stack [","]")},De.prototype.get=function(t,e){var n=this._head;for(t=u(this,t);n&&t--;)n=n.next;return n?n.value:e},De.prototype.peek=function(){return this._head&&this._head.value},De.prototype.push=function(){var t=arguments;if(0===arguments.length)return this;for(var e=this.size+arguments.length,n=this._head,r=arguments.length-1;r>=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):ze(e,n)},De.prototype.pushAll=function(t){if(t=_(t),0===t.size)return this;at(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):ze(e,n)},De.prototype.pop=function(){return this.slice(1)},De.prototype.unshift=function(){return this.push.apply(this,arguments)},De.prototype.unshiftAll=function(t){return this.pushAll(t)},De.prototype.shift=function(){return this.pop.apply(this,arguments)},De.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):Re()},De.prototype.slice=function(t,e){if(s(t,e,this.size))return this;var n=c(t,this.size),r=f(e,this.size);if(r!==this.size)return Y.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):ze(i,o)},De.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?ze(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},De.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},De.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new b(function(){if(r){var e=r.value;return r=r.next,E(t,n++,e)}return I()})},De.isStack=Ce;var Wn="@@__IMMUTABLE_STACK__@@",Xn=De.prototype;Xn[Wn]=!0,Xn.withMutations=Hn.withMutations,Xn.asMutable=Hn.asMutable,Xn.asImmutable=Hn.asImmutable,Xn.wasAltered=Hn.wasAltered;var Qn;t(Me,J),Me.of=function(){return this(arguments)},Me.fromKeys=function(t){return this(p(t).keySeq())},Me.prototype.toString=function(){return this.__toString("Set {","}")},Me.prototype.has=function(t){return this._map.has(t)},Me.prototype.add=function(t){return Le(this,this._map.set(t,!0))},Me.prototype.remove=function(t){return Le(this,this._map.remove(t))},Me.prototype.clear=function(){return Le(this,this._map.clear())},Me.prototype.union=function(){var t=an.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;n1?" by "+this._step:"")+" ]"},Ye.prototype.get=function(t,e){return this.has(t)?this._start+u(this,t)*this._step:e},Ye.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e=0&&nn?I():E(t,o++,u)})},Ye.prototype.equals=function(t){return t instanceof Ye?this._start===t._start&&this._end===t._end&&this._step===t._step:Be(this,t)};var ir;t(Je,R),Je.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Je.prototype.get=function(t,e){return this.has(t)?this._value:e},Je.prototype.includes=function(t){return W(this._value,t)},Je.prototype.slice=function(t,e){var n=this.size;return s(t,e,n)?this:new Je(this._value,f(e,n)-c(t,n))},Je.prototype.reverse=function(){return this},Je.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Je.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Je.prototype.__iterate=function(t,e){for(var n=this,r=0;rthis.size?e:this.find((function(e,n){return n===t}),void 0,e)},has:function(t){return t=u(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,y.toFactory)(g),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 M({result:t,reactorState:e})}function o(t,e){return t.withMutations((function(t){(0,R.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,D.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 I(t,[n])}))})),E(t)}))}function u(t,e){return t.withMutations((function(t){(0,R.each)(e,(function(e,n){t.update("stores",(function(t){return t.set(n,e)}))}))}))}function a(t,e,n){if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var r=t.get("state"),i=t.get("dirtyStores"),o=r.withMutations((function(r){A.default.dispatchStart(t,e,n),t.get("stores").forEach((function(o,u){var a=r.get(u),s=void 0;try{s=o.handle(a,e,n)}catch(e){throw A.default.dispatchError(t,e.message),e}if(void 0===s&&f(t,"throwOnUndefinedStoreReturnValue")){var c="Store handler must return a value, did you forget a return statement";throw A.default.dispatchError(t,c),new Error(c)}r.set(u,s),a!==s&&(i=i.add(u))})),A.default.dispatchEnd(t,r,i)})),u=t.set("state",o).set("dirtyStores",i).update("storeStates",(function(t){return I(t,i)}));return E(u)}function s(t,e){var n=[],r=(0,D.toImmutable)({}).withMutations((function(r){(0,R.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=w.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 I(t,n)}))}function c(t,e,n){var r=e;(0,z.isKeyPath)(e)&&(e=(0,C.fromKeyPath)(e));var i=t.get("nextId"),o=(0,C.getStoreDeps)(e),u=w.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,w.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,z.isKeyPath)(e)&&(0,z.isKeyPath)(r)?(0,z.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,D.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 I(t,r)})),v(t)}))}function _(t,e){var n=t.get("state");if((0,z.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,C.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");if(g(t,e))return i(b(t,e),t);var r=(0,C.getDeps)(e).map((function(e){return _(t,e).result})),o=(0,C.getComputeFn)(e).apply(null,r);return i(o,S(t,e,o))}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",w.default.Set())}function y(t){return t}function m(t,e){var n=y(e);return t.getIn(["cache",n])}function g(t,e){var n=m(t,e);if(!n)return!1;var r=n.get("storeStates");return 0!==r.size&&r.every((function(e,n){return t.getIn(["storeStates",n])===e}))}function S(t,e,n){var r=y(e),i=t.get("dispatchId"),o=(0,C.getStoreDeps)(e),u=(0,D.toImmutable)({}).withMutations((function(e){o.forEach((function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)}))}));return t.setIn(["cache",r],w.default.Map({value:n,storeStates:u,dispatchId:i}))}function b(t,e){var n=y(e);return t.getIn(["cache",n,"value"])}function E(t){return t.update("dispatchId",(function(t){return t+1}))}function I(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 O=n(3),w=r(O),T=n(9),A=r(T),D=n(5),C=n(10),z=n(11),R=n(4),M=w.default.Record({result:null,reactorState:null})},function(t,e,n){var r=n(8);e.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())},e.dispatchError=function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},e.dispatchEnd=function(t,e,n){(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())}},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(3),i=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=i;var o=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=o;var u=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,r.Map)(),storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:i});e.ReactorState=u;var a=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=a}])}))})),Ne=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},Pe=ke,Ue=Pe({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),He=Ne.Store,xe=Ne.toImmutable,Ve=new He({getInitialState:function(){return xe({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Ue.VALIDATING_AUTH_TOKEN,n),this.on(Ue.VALID_AUTH_TOKEN,r),this.on(Ue.INVALID_AUTH_TOKEN,i)}}),qe=Ne.Store,Fe=Ne.toImmutable,Ge=new qe({getInitialState:function(){return Fe({authToken:null,host:""})},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,o),this.on(Ue.LOG_OUT,u)}}),Ke=Ne.Store,Be=new Ke({getInitialState:function(){return!0},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,a)}}),Ye=Pe({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Je=Ne.Store,We=Ne.toImmutable,Xe=new Je({getInitialState:function(){return We({isStreaming:!1,hasError:!1})},initialize:function(){this.on(Ye.STREAM_START,s),this.on(Ye.STREAM_ERROR,c),this.on(Ye.LOG_OUT,f)}}),Qe=1,Ze=2,$e=3,tn=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};tn.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},tn.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},tn.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(S($e,"Connection lost"))}));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(Ze),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(Qe);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},tn.prototype.close=function(){this.closeRequested=!0,this.socket.close()},tn.prototype.getStates=function(){return this.sendMessagePromise(l()).then(b)},tn.prototype.getServices=function(){return this.sendMessagePromise(_()).then(b)},tn.prototype.getPanels=function(){return this.sendMessagePromise(d()).then(b)},tn.prototype.getConfig=function(){return this.sendMessagePromise(p()).then(b)},tn.prototype.callService=function(t,e,n){return this.sendMessagePromise(v(t,e,n))},tn.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(m(r.id)).then((function(){delete n.commands[r.id]}))}};return n.commands[r.id]=i,function(){return i.unsubscribe()}}))},tn.prototype.ping=function(){return this.sendMessagePromise(g())},tn.prototype.sendMessage=function(t){this.socket.send(JSON.stringify(t))},tn.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 en=Pe({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),nn=Ne.Store,rn=new nn({getInitialState:function(){return!0},initialize:function(){this.on(en.API_FETCH_ALL_START,(function(){return!0})),this.on(en.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(en.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(en.LOG_OUT,(function(){return!1}))}}),on=I,un=Pe({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}),an=Ne.Store,sn=Ne.toImmutable,cn=new an({getInitialState:function(){return sn({})},initialize:function(){var t=this;this.on(un.API_FETCH_SUCCESS,O),this.on(un.API_SAVE_SUCCESS,O),this.on(un.API_DELETE_SUCCESS,w),this.on(un.LOG_OUT,(function(){return t.getInitialState()}))}}),fn=Object.prototype.hasOwnProperty,hn=Object.prototype.propertyIsEnumerable,ln=A()?Object.assign:function(t,e){for(var n,r,i=arguments,o=T(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 O(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?sn({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||sn;return t.set(o,u.withMutations((function(t){for(var e=0;e6e4}function gt(t,e){var n=e.date;return n.toISOString()}function mt(){return Qr.getInitialState()}function St(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,$r({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],$r(e.map(In.fromJSON)))}))}))}function Et(){return ti.getInitialState()}function bt(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,ii(e.map(In.fromJSON)))}))}))}function It(){return oi.getInitialState()}function Ot(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(si,r)}))}function wt(){return ci.getInitialState()}function Tt(t,e){t.dispatch(Wr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function At(t,e){void 0===e&&(e=null),t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),on(t,"GET",n).then((function(e){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Wr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function Ct(t,e){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_START,{date:e}),on(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Wr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function Dt(t){var e=t.evaluate(li);return Ct(t,e)}function zt(t){t.registerStores({currentEntityHistoryDate:Qr,entityHistory:ti,isLoadingEntityHistory:ni,recentEntityHistory:oi,recentEntityHistoryUpdated:ci})}function Rt(t){t.registerStores({moreInfoEntityId:Yr})}function Mt(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 Yt(t){var e=fo[t.hassId];e&&(e.scheduleHealthCheck.clear(),e.conn.close(),fo[t.hassId]=!1)}function Jt(t,e){void 0===e&&(e={});var n=e.syncOnInitialConnect;void 0===n&&(n=!0),Yt(t);var r=t.evaluate(Mo.authToken),i="https:"===document.location.protocol?"wss://":"ws://";i+=document.location.hostname,document.location.port&&(i+=":"+document.location.port),i+="/api/websocket",b(i,{authToken:r}).then((function(e){var r=Bt((function(){return e.ping()}),so);r(),e.socket.addEventListener("message",r),fo[t.hassId]={conn:e,scheduleHealthCheck:r},co.forEach((function(n){return e.subscribeEvents(ao.bind(null,t),n)})),t.batch((function(){t.dispatch(Ye.STREAM_START),n&&io.fetchAll(t)})),e.addEventListener("disconnected",(function(){t.dispatch(Ye.STREAM_ERROR)})),e.addEventListener("ready",(function(){t.batch((function(){t.dispatch(Ye.STREAM_START),io.fetchAll(t)}))}))}))}function Wt(t){t.registerStores({streamStatus:Xe})}function Xt(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(Ue.VALIDATING_AUTH_TOKEN,{authToken:e,host:i}),io.fetchAll(t).then((function(){t.dispatch(Ue.VALID_AUTH_TOKEN,{authToken:e,host:i,rememberAuth:r}),vo.start(t,{syncOnInitialConnect:!1})}),(function(e){void 0===e&&(e={});var n=e.message;void 0===n&&(n=mo),t.dispatch(Ue.INVALID_AUTH_TOKEN,{errorMessage:n})}))}function Qt(t){t.dispatch(Ue.LOG_OUT,{})}function Zt(t){t.registerStores({authAttempt:Ve,authCurrent:Ge,rememberAuth:Be})}function $t(){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 te(){var t=new Uo({debug:!1});return t.hassId=Ho++,t}function ee(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 ne(t,e){return xo(t.attributes.entity_id.map((function(t){return e.get(t)})).filter((function(t){return!!t})))}function re(t){return on(t,"GET","error_log")}function ie(t,e){var n=e.date;return n.toISOString()}function oe(){return Jo.getInitialState()}function ue(t,e){var n=e.date,r=e.entries;return t.set(n,eu(r.map($o.fromJSON)))}function ae(){return nu.getInitialState()}function se(t,e){var n=e.date;return t.set(n,(new Date).getTime())}function ce(){return ou.getInitialState()}function fe(t,e){t.dispatch(Bo.LOGBOOK_DATE_SELECTED,{date:e})}function he(t,e){t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_START,{date:e}),on(t,"GET","logbook/"+e).then((function(n){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})}),(function(){return t.dispatch(Bo.LOGBOOK_ENTRIES_FETCH_ERROR,{})}))}function le(t){return!t||(new Date).getTime()-t>su}function pe(t){t.registerStores({currentLogbookDate:Jo,isLoadingLogbookEntries:Xo,logbookEntries:nu,logbookEntriesUpdated:ou})}function _e(t){return t.set("active",!0)}function de(t){return t.set("active",!1)}function ve(){return Su.getInitialState()}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){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",on(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(yu.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),Vn.createNotification(t,n),!1}))}function ge(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 on(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(yu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Vn.createNotification(t,n),!1}))}function me(t){t.registerStores({pushNotifications:Su})}function Se(t,e){return on(t,"POST","template",{template:e})}function Ee(t){return t.set("isListening",!0)}function be(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 Ie(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 Oe(){return Nu.getInitialState()}function we(){return Nu.getInitialState()}function Te(){return Nu.getInitialState()}function Ae(t){return Pu[t.hassId]}function Ce(t){var e=Ae(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(Lu.VOICE_TRANSMITTING,{finalTranscript:n}),tr.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(Lu.VOICE_DONE)}),(function(){t.dispatch(Lu.VOICE_ERROR)}))}}function De(t){var e=Ae(t);e&&(e.recognition.stop(),Pu[t.hassId]=!1)}function ze(t){Ce(t),De(t)}function Re(t){var e=ze.bind(null,t);e();var n=new webkitSpeechRecognition;Pu[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=Ae(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}])}))})),ke=t(je),Ne=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},Pe=Ne,Ue=Pe({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),He=ke.Store,xe=ke.toImmutable,Ve=new He({getInitialState:function(){return xe({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Ue.VALIDATING_AUTH_TOKEN,n),this.on(Ue.VALID_AUTH_TOKEN,r),this.on(Ue.INVALID_AUTH_TOKEN,i)}}),qe=ke.Store,Fe=ke.toImmutable,Ge=new qe({getInitialState:function(){return Fe({authToken:null,host:""})},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,o),this.on(Ue.LOG_OUT,u)}}),Ke=ke.Store,Be=new Ke({getInitialState:function(){return!0},initialize:function(){this.on(Ue.VALID_AUTH_TOKEN,a)}}),Ye=Pe({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Je=ke.Store,We=ke.toImmutable,Xe=new Je({getInitialState:function(){return We({isStreaming:!1,hasError:!1})},initialize:function(){this.on(Ye.STREAM_START,s),this.on(Ye.STREAM_ERROR,c),this.on(Ye.LOG_OUT,f)}}),Qe=1,Ze=2,$e=3,tn=function(t,e){this.url=t,this.options=e||{},this.commandId=1,this.commands={},this.connectionTries=0,this.eventListeners={},this.closeRequested=!1};tn.prototype.addEventListener=function(t,e){var n=this.eventListeners[t];n||(n=this.eventListeners[t]=[]),n.push(e)},tn.prototype.fireEvent=function(t){var e=this;(this.eventListeners[t]||[]).forEach((function(t){return t(e)}))},tn.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(S($e,"Connection lost"))}));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(Ze),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(Qe);var e=1e3*Math.min(t.connectionTries,5);setTimeout((function(){return t.connect()}),e)}}))})},tn.prototype.close=function(){this.closeRequested=!0,this.socket.close()},tn.prototype.getStates=function(){return this.sendMessagePromise(l()).then(E)},tn.prototype.getServices=function(){return this.sendMessagePromise(_()).then(E)},tn.prototype.getPanels=function(){return this.sendMessagePromise(d()).then(E)},tn.prototype.getConfig=function(){return this.sendMessagePromise(p()).then(E)},tn.prototype.callService=function(t,e,n){return this.sendMessagePromise(v(t,e,n))},tn.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()}}))},tn.prototype.ping=function(){return this.sendMessagePromise(m())},tn.prototype.sendMessage=function(t){this.socket.send(JSON.stringify(t))},tn.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 en=Pe({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),nn=ke.Store,rn=new nn({getInitialState:function(){return!0},initialize:function(){this.on(en.API_FETCH_ALL_START,(function(){return!0})),this.on(en.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(en.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(en.LOG_OUT,(function(){return!1}))}}),on=I,un=Pe({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}),an=ke.Store,sn=ke.toImmutable,cn=new an({getInitialState:function(){return sn({})},initialize:function(){var t=this;this.on(un.API_FETCH_SUCCESS,O),this.on(un.API_SAVE_SUCCESS,O),this.on(un.API_DELETE_SUCCESS,w),this.on(un.LOG_OUT,(function(){return t.getInitialState()}))}}),fn=Object.prototype.hasOwnProperty,hn=Object.prototype.propertyIsEnumerable,ln=A()?Object.assign:function(t,e){for(var n,r,i=arguments,o=T(t),u=1;uk=>_a?Oc=7E9v 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 V@ZJIaf|UH*{{d&`-;hPl0RU64V(tI{ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index f375a93bec7..04ece1856c2 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 +n&&e.updateNativeStyleProperties(document.documentElement,this.customStyle)}};return r}(),function(){"use strict";var e=Polymer.Base.serializeValueToAttribute,t=Polymer.StyleProperties,n=Polymer.StyleTransformer,r=Polymer.StyleDefaults,s=Polymer.Settings.useNativeShadow,i=Polymer.Settings.useNativeCSSProperties;Polymer.Base._addFeature({_prepStyleProperties:function(){i||(this._ownStylePropertyNames=this._styles&&this._styles.length?t.decorateStyles(this._styles,this):null)},customStyle:null,getComputedStyleValue:function(e){return i||this._styleProperties||this._computeStyleProperties(),!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 a8fa716d1ea9f89ab4ceec7177870918a4925c6a..edf06e778e90a809b3c905facab70d1238c19d30 100644 GIT binary patch delta 98035 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

esDpo0@5lhdPpx~>syH;(o4wj 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 delta 97362 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