From 45bbe75d296ea28d3a4edd12aab3e07c6a3fca3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Feb 2019 16:45:14 -0800 Subject: [PATCH 01/58] Bumped version to 0.89.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54901feb73b..0af0b220ecc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1c889cfcc36e196c332e99eea647f6d6f2d03aa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Feb 2019 17:43:36 -0800 Subject: [PATCH 02/58] Updated frontend to 20190228.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7ef030e87d7..614b5228f60 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190227.0'] +REQUIREMENTS = ['home-assistant-frontend==20190228.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index e4336a03ffe..780addec717 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190227.0 +home-assistant-frontend==20190228.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index deed85b20ff..6c2a15e59d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190227.0 +home-assistant-frontend==20190228.0 # homeassistant.components.homekit_controller homekit==0.12.2 From e6cbdf0645bf4500ada5b4b1508217f5ae538025 Mon Sep 17 00:00:00 2001 From: Ben Randall Date: Thu, 28 Feb 2019 09:27:40 -0800 Subject: [PATCH 03/58] Add PLATFORM_SCHEMA_BASE to telegram_bot component (#21155) --- homeassistant/components/telegram_bot/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index c55b27e97a6..78d45535c48 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -84,6 +84,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PROXY_PARAMS): dict, }) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) + BASE_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(ATTR_PARSER): cv.string, From 9e140d27bf2c37248daed59b1e9b35eed80a4d82 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 27 Feb 2019 21:04:55 +0100 Subject: [PATCH 04/58] Fix deCONZ retry mechanism for setup From aa546b5a1f25b3651b95fc4b8f1619b0900ea044 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Feb 2019 15:28:20 -0700 Subject: [PATCH 05/58] Add watchdog to Ambient PWS (#21507) * Add watchdog to Ambient PWS * Better labeling * Owner comments --- .../components/ambient_station/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index a8e1c7fb292..70f6ce9fbba 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -27,6 +27,7 @@ _LOGGER = logging.getLogger(__name__) DATA_CONFIG = 'config' DEFAULT_SOCKET_MIN_RETRY = 15 +DEFAULT_WATCHDOG_SECONDS = 5 * 60 TYPE_24HOURRAININ = '24hourrainin' TYPE_BAROMABSIN = 'baromabsin' @@ -296,6 +297,7 @@ class AmbientStation: """Initialize.""" self._config_entry = config_entry self._hass = hass + self._watchdog_listener = None self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client self.monitored_conditions = monitored_conditions @@ -305,9 +307,18 @@ class AmbientStation: """Register handlers and connect to the websocket.""" from aioambient.errors import WebsocketError + async def _ws_reconnect(event_time): + """Forcibly disconnect from and reconnect to the websocket.""" + _LOGGER.debug('Watchdog expired; forcing socket reconnection') + await self.client.websocket.disconnect() + await self.client.websocket.connect() + def on_connect(): """Define a handler to fire when the websocket is connected.""" _LOGGER.info('Connected to websocket') + _LOGGER.debug('Watchdog starting') + self._watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) def on_data(data): """Define a handler to fire when the data is received.""" @@ -317,6 +328,11 @@ class AmbientStation: self.stations[mac_address][ATTR_LAST_DATA] = data async_dispatcher_send(self._hass, TOPIC_UPDATE) + _LOGGER.debug('Resetting watchdog') + self._watchdog_listener() + self._watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) + def on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" _LOGGER.info('Disconnected from websocket') From 26a534a67c42f7d7a84633121d168425219e3fa6 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 28 Feb 2019 19:25:31 +0100 Subject: [PATCH 06/58] Improve new Sonos snapshot/restore (#21509) * Fine-tune new Sonos snapshot/restore * Move into class --- .../components/sonos/media_player.py | 145 ++++++++++-------- tests/components/sonos/test_media_player.py | 4 +- 2 files changed, 86 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4a02cf2676f..e0f881f723d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -197,9 +197,11 @@ def _setup_platform(hass, config, add_entities, discovery_info): with hass.data[DATA_SONOS].topology_lock: if service.service == SERVICE_SNAPSHOT: - snapshot(entities, service.data[ATTR_WITH_GROUP]) + SonosEntity.snapshot_multi( + entities, service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: - restore(entities, service.data[ATTR_WITH_GROUP]) + SonosEntity.restore_multi( + entities, service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_JOIN: master = [e for e in hass.data[DATA_SONOS].entities if e.entity_id == service.data[ATTR_MASTER]] @@ -357,6 +359,7 @@ class SonosEntity(MediaPlayerDevice): self._favorites = None self._soco_snapshot = None self._snapshot_group = None + self._restore_pending = False self._set_basic_information() @@ -724,6 +727,9 @@ class SonosEntity(MediaPlayerDevice): pass if self.unique_id == coordinator_uid: + if self._restore_pending: + self.restore() + sonos_group = [] for uid in (coordinator_uid, *slave_uids): entity = _get_entity_from_soco_uid(self.hass, uid) @@ -974,6 +980,82 @@ class SonosEntity(MediaPlayerDevice): self.soco.unjoin() self._coordinator = None + @soco_error() + def snapshot(self, with_group): + """Snapshot the state of a player.""" + from pysonos.snapshot import Snapshot + + self._soco_snapshot = Snapshot(self.soco) + self._soco_snapshot.snapshot() + if with_group: + self._snapshot_group = self._sonos_group.copy() + else: + self._snapshot_group = None + + @soco_error() + def restore(self): + """Restore a snapshotted state to a player.""" + from pysonos.exceptions import SoCoException + + try: + # pylint: disable=protected-access + self.soco._zgs_cache.clear() + self._soco_snapshot.restore() + except (TypeError, AttributeError, SoCoException) as ex: + # Can happen if restoring a coordinator onto a current slave + _LOGGER.warning("Error on restore %s: %s", self.entity_id, ex) + + self._soco_snapshot = None + self._snapshot_group = None + self._restore_pending = False + + @staticmethod + def snapshot_multi(entities, with_group): + """Snapshot all the entities and optionally their groups.""" + # pylint: disable=protected-access + # Find all affected players + entities = set(entities) + if with_group: + for entity in list(entities): + entities.update(entity._sonos_group) + + for entity in entities: + entity.snapshot(with_group) + + @staticmethod + def restore_multi(entities, with_group): + """Restore snapshots for all the entities.""" + # pylint: disable=protected-access + # Find all affected players + entities = set(e for e in entities if e._soco_snapshot) + if with_group: + for entity in [e for e in entities if e._snapshot_group]: + entities.update(entity._snapshot_group) + + # Pause all current coordinators + for entity in (e for e in entities if e.is_coordinator): + if entity.state == STATE_PLAYING: + entity.media_pause() + + # Bring back the original group topology + if with_group: + for entity in (e for e in entities if e._snapshot_group): + if entity._snapshot_group[0] == entity: + entity.join(entity._snapshot_group) + + # Restore slaves + for entity in (e for e in entities if not e.is_coordinator): + entity.restore() + + # Restore coordinators (or delay if moving from slave) + for entity in (e for e in entities if e.is_coordinator): + if entity._sonos_group[0] == entity: + # Was already coordinator + entity.restore() + else: + # Await coordinator role + entity._restore_pending = True + @soco_error() @soco_coordinator def set_sleep_timer(self, sleep_time): @@ -1033,62 +1115,3 @@ class SonosEntity(MediaPlayerDevice): attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance return attributes - - -@soco_error() -def snapshot(entities, with_group): - """Snapshot all the entities and optionally their groups.""" - # pylint: disable=protected-access - from pysonos.snapshot import Snapshot - - # Find all affected players - entities = set(entities) - if with_group: - for entity in list(entities): - entities.update(entity._sonos_group) - - for entity in entities: - entity._soco_snapshot = Snapshot(entity.soco) - entity._soco_snapshot.snapshot() - if with_group: - entity._snapshot_group = entity._sonos_group.copy() - else: - entity._snapshot_group = None - - -@soco_error() -def restore(entities, with_group): - """Restore snapshots for all the entities.""" - # pylint: disable=protected-access - from pysonos.exceptions import SoCoException - - # Find all affected players - entities = set(e for e in entities if e._soco_snapshot) - if with_group: - for entity in [e for e in entities if e._snapshot_group]: - entities.update(entity._snapshot_group) - - # Pause all current coordinators - for entity in (e for e in entities if e.is_coordinator): - if entity.state == STATE_PLAYING: - entity.media_pause() - - # Bring back the original group topology and clear pysonos cache - if with_group: - for entity in (e for e in entities if e._snapshot_group): - if entity._snapshot_group[0] == entity: - entity.join(entity._snapshot_group) - entity.soco._zgs_cache.clear() - - # Restore slaves, then coordinators - slaves = [e for e in entities if not e.is_coordinator] - coordinators = [e for e in entities if e.is_coordinator] - for entity in slaves + coordinators: - try: - entity._soco_snapshot.restore() - except (TypeError, AttributeError, SoCoException) as ex: - # Can happen if restoring a coordinator onto a current slave - _LOGGER.warning("Error on restore %s: %s", entity.entity_id, ex) - - entity._soco_snapshot = None - entity._snapshot_group = None diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 55743c4f843..798c92eddad 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -328,7 +328,7 @@ class TestSonosMediaPlayer(unittest.TestCase): snapshotMock.return_value = True entity.soco.group = mock.MagicMock() entity.soco.group.members = [e.soco for e in entities] - sonos.snapshot(entities, True) + sonos.SonosEntity.snapshot_multi(entities, True) assert snapshotMock.call_count == 1 assert snapshotMock.call_args == mock.call() @@ -350,6 +350,6 @@ class TestSonosMediaPlayer(unittest.TestCase): entity._snapshot_group = mock.MagicMock() entity._snapshot_group.members = [e.soco for e in entities] entity._soco_snapshot = Snapshot(entity.soco) - sonos.restore(entities, True) + sonos.SonosEntity.restore_multi(entities, True) assert restoreMock.call_count == 1 assert restoreMock.call_args == mock.call() From 4fe9f966adba17d5eff3bf823181445944a6e957 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Feb 2019 21:35:14 -0800 Subject: [PATCH 07/58] Fix lint (#21520) --- homeassistant/components/person/__init__.py | 21 +++++++++++--------- homeassistant/components/sensor/airvisual.py | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index c4af4a699cd..622ca0608ac 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,6 +2,7 @@ from collections import OrderedDict from itertools import chain import logging +from typing import Optional import uuid import voluptuous as vol @@ -13,7 +14,7 @@ from homeassistant.const import ( ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY, CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME) -from homeassistant.core import callback, Event +from homeassistant.core import callback, Event, State from homeassistant.auth import EVENT_USER_REMOVED import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -377,11 +378,6 @@ class Person(RestoreEntity): """Handle the device tracker state changes.""" self._update_state() - def _get_latest(self, prev, curr): - return curr \ - if prev is None or curr.last_updated > prev.last_updated \ - else prev - @callback def _update_state(self): """Update the state.""" @@ -393,11 +389,11 @@ class Person(RestoreEntity): continue if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: - latest_gps = self._get_latest(latest_gps, state) + latest_gps = _get_latest(latest_gps, state) elif state.state == STATE_HOME: - latest_home = self._get_latest(latest_home, state) + latest_home = _get_latest(latest_home, state) elif state.state == STATE_NOT_HOME: - latest_not_home = self._get_latest(latest_not_home, state) + latest_not_home = _get_latest(latest_not_home, state) if latest_home: latest = latest_home @@ -508,3 +504,10 @@ async def ws_delete_person(hass: HomeAssistantType, manager = hass.data[DOMAIN] # type: PersonManager await manager.async_delete_person(msg['person_id']) connection.send_result(msg['id']) + + +def _get_latest(prev: Optional[State], curr: State): + """Get latest state.""" + if prev is None or curr.last_updated > prev.last_updated: + return curr + return prev diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index 46457a17ebb..e13fb924041 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -141,7 +141,7 @@ async def async_setup_platform( "Using city, state, and country: %s, %s, %s", city, state, country) location_id = ','.join((city, state, country)) data = AirVisualData( - Client(config[CONF_API_KEY], websession), + Client(websession, api_key=config[CONF_API_KEY]), city=city, state=state, country=country, @@ -152,7 +152,7 @@ async def async_setup_platform( "Using latitude and longitude: %s, %s", latitude, longitude) location_id = ','.join((str(latitude), str(longitude))) data = AirVisualData( - Client(config[CONF_API_KEY], websession), + Client(websession, api_key=config[CONF_API_KEY]), latitude=latitude, longitude=longitude, show_on_map=config[CONF_SHOW_ON_MAP], From 238c4247d91a8a057e6db81adb9fcc65efe3a01e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Feb 2019 10:00:17 -0800 Subject: [PATCH 08/58] Only use a single store instance (#21521) --- homeassistant/components/frontend/storage.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index f01abc79e8e..17aae14c820 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -11,7 +11,7 @@ STORAGE_KEY_USER_DATA = 'frontend.user_data_{}' async def async_setup_frontend_storage(hass): """Set up frontend storage.""" - hass.data[DATA_STORAGE] = {} + hass.data[DATA_STORAGE] = ({}, {}) hass.components.websocket_api.async_register_command( websocket_set_user_data ) @@ -25,12 +25,16 @@ def with_store(orig_func): @wraps(orig_func) async def with_store_func(hass, connection, msg): """Provide user specific data and store to function.""" - store = hass.helpers.storage.Store( - STORAGE_VERSION_USER_DATA, - STORAGE_KEY_USER_DATA.format(connection.user.id) - ) - data = hass.data[DATA_STORAGE] + stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id + store = stores.get(user_id) + + if store is None: + store = stores[user_id] = hass.helpers.storage.Store( + STORAGE_VERSION_USER_DATA, + STORAGE_KEY_USER_DATA.format(connection.user.id) + ) + if user_id not in data: data[user_id] = await store.async_load() or {} From eda2290d473d96314b410235d437fb75ca1a7a27 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Feb 2019 10:01:10 -0800 Subject: [PATCH 09/58] Allow skip-pip applied to HA core (#21527) --- homeassistant/bootstrap.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ca01610bcf9..eef36b026e1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -85,6 +85,11 @@ async def async_from_config_dict(config: Dict[str, Any], async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + hass.config.skip_pip = skip_pip + if skip_pip: + _LOGGER.warning("Skipping pip installation of required modules. " + "This may cause issues") + core_config = config.get(core.DOMAIN, {}) has_api_password = bool(config.get('http', {}).get('api_password')) trusted_networks = config.get('http', {}).get('trusted_networks') @@ -104,11 +109,6 @@ async def async_from_config_dict(config: Dict[str, Any], await hass.async_add_executor_job( conf_util.process_ha_config_upgrade, hass) - hass.config.skip_pip = skip_pip - if skip_pip: - _LOGGER.warning("Skipping pip installation of required modules. " - "This may cause issues") - # Make a copy because we are mutating it. config = OrderedDict(config) From 6f2dd21516c0858f2f07438f2a93a8b67489570a Mon Sep 17 00:00:00 2001 From: Victor Vostrikov <1998617+gorynychzmey@users.noreply.github.com> Date: Thu, 28 Feb 2019 13:05:39 +0100 Subject: [PATCH 10/58] Updated variable name for readability (#21528) --- homeassistant/components/person/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 622ca0608ac..e6f83b80ba4 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -381,7 +381,7 @@ class Person(RestoreEntity): @callback def _update_state(self): """Update the state.""" - latest_home = latest_not_home = latest_gps = latest = None + latest_non_gps_home = latest_not_home = latest_gps = latest = None for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []): state = self.hass.states.get(entity_id) @@ -391,12 +391,12 @@ class Person(RestoreEntity): if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: latest_gps = _get_latest(latest_gps, state) elif state.state == STATE_HOME: - latest_home = _get_latest(latest_home, state) + latest_non_gps_home = _get_latest(latest_non_gps_home, state) elif state.state == STATE_NOT_HOME: latest_not_home = _get_latest(latest_not_home, state) - if latest_home: - latest = latest_home + if latest_non_gps_home: + latest = latest_non_gps_home elif latest_gps: latest = latest_gps else: From b05062e9d99eecfbf17f17e4ba8718d4d682a350 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 28 Feb 2019 18:26:54 +0100 Subject: [PATCH 11/58] Add missing retain option to mqtt.climate configuration schema (#21536) --- homeassistant/components/mqtt/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 957e1aadfb7..7be47185322 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -93,6 +93,7 @@ TEMPLATE_KEYS = ( SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) PLATFORM_SCHEMA = SCHEMA_BASE.extend({ + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic, From 97b93bcf7bb378f409363bef58d37b53f19776da Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Feb 2019 10:10:21 -0800 Subject: [PATCH 12/58] Fix warning (#21538) --- homeassistant/components/http/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f57068081a5..4928ae2ab17 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -55,6 +55,9 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1 def trusted_networks_deprecated(value): """Warn user trusted_networks config is deprecated.""" + if not value: + return value + _LOGGER.warning( "Configuring trusted_networks via the http component has been" " deprecated. Use the trusted networks auth provider instead." From 0f09c0287527005ecf72a7a50b15b2c0ff4b4559 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Feb 2019 15:17:10 -0700 Subject: [PATCH 13/58] Fix incorrect pyairvisual call (#21542) --- homeassistant/components/sensor/airvisual.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index e13fb924041..b9e7a3315e3 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -278,11 +278,11 @@ class AirVisualData: try: if self.city and self.state and self.country: - resp = await self._client.data.city( + resp = await self._client.api.city( self.city, self.state, self.country) self.longitude, self.latitude = resp['location']['coordinates'] else: - resp = await self._client.data.nearest_city( + resp = await self._client.api.nearest_city( self.latitude, self.longitude) _LOGGER.debug("New data retrieved: %s", resp) From ed28482311f053b173310daec0a3a6b98aa36124 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Feb 2019 17:58:23 -0800 Subject: [PATCH 14/58] Bumped version to 0.89.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0af0b220ecc..a3dfff9a0b0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 43f85f70536a3cab9c9b3566d890c50348e0d04e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Mar 2019 21:55:51 -0800 Subject: [PATCH 15/58] Updated frontend to 20190303.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 614b5228f60..fbbea13f026 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190228.0'] +REQUIREMENTS = ['home-assistant-frontend==20190303.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 780addec717..3ede6e21e36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190228.0 +home-assistant-frontend==20190303.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c2a15e59d2..2892b759c99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190228.0 +home-assistant-frontend==20190303.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 73675d5a4840e8048eff370f2c5443cf74f68fa4 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 1 Mar 2019 23:08:20 -0800 Subject: [PATCH 16/58] mobile_app component (#21475) * Initial pass of a mobile_app component * Fully support encryption, validation for the webhook payloads, and other general improvements * Return same format as original API calls * Minor encryption fixes, logging improvements * Migrate Owntracks to use the superior PyNaCl instead of libnacl, mark it as a requirement in mobile_app * Add mobile_app to .coveragerc * Dont manually b64decode on OT * Initial requested changes * Round two of fixes * Initial mobile_app tests * Dont allow making registration requests for same/existing device * Test formatting fixes * Add mobile_app to default_config * Add some more keys allowed in registration payloads * Add support for getting a single device, updating a device, getting all devices. Also change from /api/mobile_app/register to /api/mobile_app/devices * Change device_id to fingerprint * Next round of changes * Add keyword args and pass context on all relevant calls * Remove SingleDeviceView in favor of webhook type to update registration * Only allow some properties to be updated on registrations, rename integration_data to app_data * Add call service test, ensure events actually fire, only run the encryption tests if sodium is installed * pylint * Fix OwnTracks test * Fix iteration of devices and remove device_for_webhook_id --- .coveragerc | 3 +- .../components/default_config/__init__.py | 1 + .../components/mobile_app/__init__.py | 355 ++++++++++++++++++ .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 2 +- .../components/owntracks/device_tracker.py | 10 +- requirements_all.txt | 7 +- requirements_test_all.txt | 4 + script/gen_requirements_all.py | 1 + .../device_tracker/test_owntracks.py | 26 +- tests/components/mobile_app/__init__.py | 1 + tests/components/mobile_app/test_init.py | 275 ++++++++++++++ 12 files changed, 666 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/mobile_app/__init__.py create mode 100644 tests/components/mobile_app/__init__.py create mode 100644 tests/components/mobile_app/test_init.py diff --git a/.coveragerc b/.coveragerc index f8829939682..bbed9b7e742 100644 --- a/.coveragerc +++ b/.coveragerc @@ -320,6 +320,7 @@ omit = homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/meteo_france/* + homeassistant/components/mobile_app/* homeassistant/components/mochad/* homeassistant/components/modbus/* homeassistant/components/mychevy/* @@ -384,7 +385,7 @@ omit = homeassistant/components/point/* homeassistant/components/prometheus/* homeassistant/components/ps4/__init__.py - homeassistant/components/ps4/media_player.py + homeassistant/components/ps4/media_player.py homeassistant/components/qwikswitch/* homeassistant/components/rachio/* homeassistant/components/rainbird/* diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index d56cf9a4ee8..badc403c7c8 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -11,6 +11,7 @@ DEPENDENCIES = ( 'history', 'logbook', 'map', + 'mobile_app', 'person', 'script', 'sun', diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py new file mode 100644 index 00000000000..19a81b4aa45 --- /dev/null +++ b/homeassistant/components/mobile_app/__init__.py @@ -0,0 +1,355 @@ +"""Support for native mobile apps.""" +import logging +import json +from functools import partial + +import voluptuous as vol +from aiohttp.web import json_response, Response +from aiohttp.web_exceptions import HTTPBadRequest + +from homeassistant import config_entries +from homeassistant.auth.util import generate_secret +import homeassistant.core as ha +from homeassistant.core import Context +from homeassistant.components import webhook +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, SERVICE_SEE as DEVICE_TRACKER_SEE, + SERVICE_SEE_PAYLOAD_SCHEMA as SEE_SCHEMA) +from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + HTTP_BAD_REQUEST, HTTP_CREATED, + HTTP_INTERNAL_SERVER_ERROR, CONF_WEBHOOK_ID) +from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound, + TemplateError) +from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.typing import HomeAssistantType + +REQUIREMENTS = ['PyNaCl==1.3.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mobile_app' + +DEPENDENCIES = ['device_tracker', 'http', 'webhook'] + +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CONF_SECRET = 'secret' +CONF_USER_ID = 'user_id' + +ATTR_APP_DATA = 'app_data' +ATTR_APP_ID = 'app_id' +ATTR_APP_NAME = 'app_name' +ATTR_APP_VERSION = 'app_version' +ATTR_DEVICE_NAME = 'device_name' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_MODEL = 'model' +ATTR_OS_VERSION = 'os_version' +ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption' + +ATTR_EVENT_DATA = 'event_data' +ATTR_EVENT_TYPE = 'event_type' + +ATTR_TEMPLATE = 'template' +ATTR_TEMPLATE_VARIABLES = 'variables' + +ATTR_WEBHOOK_DATA = 'data' +ATTR_WEBHOOK_ENCRYPTED = 'encrypted' +ATTR_WEBHOOK_ENCRYPTED_DATA = 'encrypted_data' +ATTR_WEBHOOK_TYPE = 'type' + +WEBHOOK_TYPE_CALL_SERVICE = 'call_service' +WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' +WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location' +WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' + +WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_UPDATE_REGISTRATION] + +REGISTER_DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_ID): cv.string, + vol.Optional(ATTR_APP_NAME): cv.string, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, +}) + +UPDATE_DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, +}) + +WEBHOOK_PAYLOAD_SCHEMA = vol.Schema({ + vol.Required(ATTR_WEBHOOK_TYPE): vol.In(WEBHOOK_TYPES), + vol.Required(ATTR_WEBHOOK_DATA, default={}): dict, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, +}) + +CALL_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_DOMAIN): cv.string, + vol.Required(ATTR_SERVICE): cv.string, + vol.Optional(ATTR_SERVICE_DATA, default={}): dict, +}) + +FIRE_EVENT_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_EVENT_DATA, default={}): dict, +}) + +RENDER_TEMPLATE_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEMPLATE): cv.string, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, +}) + +WEBHOOK_SCHEMAS = { + WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, + WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, + WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, + WEBHOOK_TYPE_UPDATE_LOCATION: SEE_SCHEMA, + WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_DEVICE_SCHEMA, +} + + +def get_cipher(): + """Return decryption function and length of key. + + Async friendly. + """ + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + def decrypt(ciphertext, key): + """Decrypt ciphertext using key.""" + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) + + +def _decrypt_payload(key, ciphertext): + """Decrypt encrypted payload.""" + try: + keylen, decrypt = get_cipher() + except OSError: + _LOGGER.warning( + "Ignoring encrypted payload because libsodium not installed") + return None + + if key is None: + _LOGGER.warning( + "Ignoring encrypted payload because no decryption key known") + return None + + key = key.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + try: + message = decrypt(ciphertext, key) + message = json.loads(message.decode("utf-8")) + _LOGGER.debug("Successfully decrypted mobile_app payload") + return message + except ValueError: + _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") + return None + + +def context(device): + """Generate a context from a request.""" + return Context(user_id=device[CONF_USER_ID]) + + +async def handle_webhook(store, hass: HomeAssistantType, webhook_id: str, + request): + """Handle webhook callback.""" + device = hass.data[DOMAIN][webhook_id] + + try: + req_data = await request.json() + except ValueError: + _LOGGER.warning('Received invalid JSON from mobile_app') + return json_response([], status=HTTP_BAD_REQUEST) + + try: + req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(req_data, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return Response(status=200) + + webhook_type = req_data[ATTR_WEBHOOK_TYPE] + + webhook_payload = req_data.get(ATTR_WEBHOOK_DATA, {}) + + if req_data[ATTR_WEBHOOK_ENCRYPTED]: + enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] + webhook_payload = _decrypt_payload(device[CONF_SECRET], enc_data) + + try: + data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(webhook_payload, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_CALL_SERVICE: + try: + await hass.services.async_call(data[ATTR_DOMAIN], + data[ATTR_SERVICE], + data[ATTR_SERVICE_DATA], + blocking=True, + context=context(device)) + except (vol.Invalid, ServiceNotFound): + raise HTTPBadRequest() + + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_FIRE_EVENT: + event_type = data[ATTR_EVENT_TYPE] + hass.bus.async_fire(event_type, data[ATTR_EVENT_DATA], + ha.EventOrigin.remote, context=context(device)) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: + try: + tpl = template.Template(data[ATTR_TEMPLATE], hass) + rendered = tpl.async_render(data.get(ATTR_TEMPLATE_VARIABLES)) + return json_response({"rendered": rendered}) + except (ValueError, TemplateError) as ex: + return json_response(({"error": ex}), status=HTTP_BAD_REQUEST) + + if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: + await hass.services.async_call(DEVICE_TRACKER_DOMAIN, + DEVICE_TRACKER_SEE, data, + blocking=True, context=context(device)) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: + data[ATTR_APP_ID] = device[ATTR_APP_ID] + data[ATTR_APP_NAME] = device[ATTR_APP_NAME] + data[ATTR_SUPPORTS_ENCRYPTION] = device[ATTR_SUPPORTS_ENCRYPTION] + data[CONF_SECRET] = device[CONF_SECRET] + data[CONF_USER_ID] = device[CONF_USER_ID] + data[CONF_WEBHOOK_ID] = device[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][webhook_id] = data + + try: + await store.async_save(hass.data[DOMAIN]) + except HomeAssistantError as ex: + _LOGGER.error("Error updating mobile_app registration: %s", ex) + return Response(status=200) + + return json_response(safe_device(data)) + + +def supports_encryption(): + """Test if we support encryption.""" + try: + import nacl # noqa pylint: disable=unused-import + return True + except OSError: + return False + + +def safe_device(device: dict): + """Return a device without webhook_id or secret.""" + return { + ATTR_APP_DATA: device[ATTR_APP_DATA], + ATTR_APP_ID: device[ATTR_APP_ID], + ATTR_APP_NAME: device[ATTR_APP_NAME], + ATTR_APP_VERSION: device[ATTR_APP_VERSION], + ATTR_DEVICE_NAME: device[ATTR_DEVICE_NAME], + ATTR_MANUFACTURER: device[ATTR_MANUFACTURER], + ATTR_MODEL: device[ATTR_MODEL], + ATTR_OS_VERSION: device[ATTR_OS_VERSION], + ATTR_SUPPORTS_ENCRYPTION: device[ATTR_SUPPORTS_ENCRYPTION], + } + + +def register_device_webhook(hass: HomeAssistantType, store, device): + """Register the webhook for a device.""" + device_name = 'Mobile App: {}'.format(device[ATTR_DEVICE_NAME]) + webhook_id = device[CONF_WEBHOOK_ID] + webhook.async_register(hass, DOMAIN, device_name, webhook_id, + partial(handle_webhook, store)) + + +async def async_setup(hass, config): + """Set up the mobile app component.""" + conf = config.get(DOMAIN) + + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + app_config = await store.async_load() + if app_config is None: + app_config = {} + + hass.data[DOMAIN] = app_config + + for device in app_config.values(): + register_device_webhook(hass, store, device) + + if conf is not None: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + + hass.http.register_view(DevicesView(store)) + + return True + + +async def async_setup_entry(hass, entry): + """Set up an mobile_app entry.""" + return True + + +class DevicesView(HomeAssistantView): + """A view that accepts device registration requests.""" + + url = '/api/mobile_app/devices' + name = 'api:mobile_app:register-device' + + def __init__(self, store): + """Initialize the view.""" + self._store = store + + @RequestDataValidator(REGISTER_DEVICE_SCHEMA) + async def post(self, request, data): + """Handle the POST request for device registration.""" + hass = request.app['hass'] + + resp = {} + + webhook_id = generate_secret() + + data[CONF_WEBHOOK_ID] = resp[CONF_WEBHOOK_ID] = webhook_id + + if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): + secret = generate_secret(16) + + data[CONF_SECRET] = resp[CONF_SECRET] = secret + + data[CONF_USER_ID] = request['hass_user'].id + + hass.data[DOMAIN][webhook_id] = data + + try: + await self._store.async_save(hass.data[DOMAIN]) + except HomeAssistantError: + return self.json_message("Error saving device.", + HTTP_INTERNAL_SERVER_ERROR) + + register_device_webhook(hass, self._store, data) + + return self.json(resp, status_code=HTTP_CREATED) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index cc918dcf674..c0d3d152270 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -16,7 +16,7 @@ from homeassistant.setup import async_when_setup from .config_flow import CONF_SECRET -REQUIREMENTS = ['libnacl==1.6.1'] +REQUIREMENTS = ['PyNaCl==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 6818efbbf75..59e8c4825df 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -9,7 +9,7 @@ CONF_SECRET = 'secret' def supports_encryption(): """Test if we support encryption.""" try: - import libnacl # noqa pylint: disable=unused-import + import nacl # noqa pylint: disable=unused-import return True except OSError: return False diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index e85ebbe6fe1..be8698a47b1 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ """ -import base64 import json import logging @@ -37,13 +36,13 @@ def get_cipher(): Async friendly. """ - from libnacl import crypto_secretbox_KEYBYTES as KEYLEN - from libnacl.secret import SecretBox + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext) - return (KEYLEN, decrypt) + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) def _parse_topic(topic, subscribe_topic): @@ -141,7 +140,6 @@ def _decrypt_payload(secret, topic, ciphertext): key = key.ljust(keylen, b'\0') try: - ciphertext = base64.b64decode(ciphertext) message = decrypt(ciphertext, key) message = message.decode("utf-8") _LOGGER.debug("Decrypted payload: %s", message) diff --git a/requirements_all.txt b/requirements_all.txt index 3ede6e21e36..79e68a873ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,6 +50,10 @@ PyMVGLive==1.1.4 # homeassistant.components.arduino PyMata==2.14 +# homeassistant.components.mobile_app +# homeassistant.components.owntracks +PyNaCl==1.3.0 + # homeassistant.auth.mfa_modules.totp PyQRCode==1.2.1 @@ -608,9 +612,6 @@ konnected==0.1.4 # homeassistant.components.eufy lakeside==0.12 -# homeassistant.components.owntracks -libnacl==1.6.1 - # homeassistant.components.dyson libpurecoollink==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2892b759c99..0840ee8f710 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,6 +21,10 @@ requests_mock==1.5.2 # homeassistant.components.homekit HAP-python==2.4.2 +# homeassistant.components.mobile_app +# homeassistant.components.owntracks +PyNaCl==1.3.0 + # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 09eb9f21d4a..7db76b1361b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -108,6 +108,7 @@ TEST_REQUIREMENTS = ( 'pyupnp-async', 'pywebpush', 'pyHS100', + 'PyNaCl', 'regenmaschine', 'restrictedpython', 'rflink', diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 1ac3fc4a194..8e868296703 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -1295,18 +1295,25 @@ async def test_unsupported_message(hass, context): def generate_ciphers(secret): """Generate test ciphers for the DEFAULT_LOCATION_MESSAGE.""" - # libnacl ciphertext generation will fail if the module + # PyNaCl ciphertext generation will fail if the module # cannot be imported. However, the test for decryption # also relies on this library and won't be run without it. import pickle import base64 try: - from libnacl import crypto_secretbox_KEYBYTES as KEYLEN - from libnacl.secret import SecretBox - key = secret.encode("utf-8")[:KEYLEN].ljust(KEYLEN, b'\0') - ctxt = base64.b64encode(SecretBox(key).encrypt(json.dumps( - DEFAULT_LOCATION_MESSAGE).encode("utf-8"))).decode("utf-8") + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + keylen = SecretBox.KEY_SIZE + key = secret.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + msg = json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8") + + ctxt = SecretBox(key).encrypt(msg, + encoder=Base64Encoder).decode("utf-8") except (ImportError, OSError): ctxt = '' @@ -1341,7 +1348,8 @@ def mock_cipher(): def mock_decrypt(ciphertext, key): """Decrypt/unpickle.""" import pickle - (mkey, plaintext) = pickle.loads(ciphertext) + import base64 + (mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext)) if key != mkey: raise ValueError() return plaintext @@ -1443,9 +1451,9 @@ async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: # pylint: disable=unused-import - import libnacl # noqa: F401 + import nacl # noqa: F401 except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") + pytest.skip("PyNaCl/libsodium is not installed") return await setup_owntracks(hass, { diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py new file mode 100644 index 00000000000..becdc2841f3 --- /dev/null +++ b/tests/components/mobile_app/__init__.py @@ -0,0 +1 @@ +"""Tests for mobile_app component.""" diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py new file mode 100644 index 00000000000..d0c1ae02c6c --- /dev/null +++ b/tests/components/mobile_app/test_init.py @@ -0,0 +1,275 @@ +"""Test the mobile_app_http platform.""" +import pytest + +from homeassistant.setup import async_setup_component + +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.mobile_app import (DOMAIN, STORAGE_KEY, + STORAGE_VERSION, + CONF_SECRET, CONF_USER_ID) +from homeassistant.core import callback + +from tests.common import async_mock_service + +FIRE_EVENT = { + 'type': 'fire_event', + 'data': { + 'event_type': 'test_event', + 'event_data': { + 'hello': 'yo world' + } + } +} + +RENDER_TEMPLATE = { + 'type': 'render_template', + 'data': { + 'template': 'Hello world' + } +} + +CALL_SERVICE = { + 'type': 'call_service', + 'data': { + 'domain': 'test', + 'service': 'mobile_app', + 'service_data': { + 'foo': 'bar' + } + } +} + +REGISTER = { + 'app_data': {'foo': 'bar'}, + 'app_id': 'io.homeassistant.mobile_app_test', + 'app_name': 'Mobile App Tests', + 'app_version': '1.0.0', + 'device_name': 'Test 1', + 'manufacturer': 'mobile_app', + 'model': 'Test', + 'os_version': '1.0', + 'supports_encryption': True +} + +UPDATE = { + 'app_data': {'foo': 'bar'}, + 'app_version': '2.0.0', + 'device_name': 'Test 1', + 'manufacturer': 'mobile_app', + 'model': 'Test', + 'os_version': '1.0' +} + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def mobile_app_client(hass, aiohttp_client, hass_storage, hass_admin_user): + """mobile_app mock client.""" + hass_storage[STORAGE_KEY] = { + 'version': STORAGE_VERSION, + 'data': { + 'mobile_app_test': { + CONF_SECRET: '58eb127991594dad934d1584bdee5f27', + 'supports_encryption': True, + CONF_WEBHOOK_ID: 'mobile_app_test', + 'device_name': 'Test Device', + CONF_USER_ID: hass_admin_user.id, + } + } + } + + assert hass.loop.run_until_complete(async_setup_component( + hass, DOMAIN, { + DOMAIN: {} + })) + + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +@pytest.fixture +async def mock_api_client(hass, hass_client): + """Provide an authenticated client for mobile_app to use.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + return await hass_client() + + +async def test_handle_render_template(mobile_app_client): + """Test that we render templates properly.""" + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=RENDER_TEMPLATE + ) + + assert resp.status == 200 + + json = await resp.json() + assert json == {'rendered': 'Hello world'} + + +async def test_handle_call_services(hass, mobile_app_client): + """Test that we call services properly.""" + calls = async_mock_service(hass, 'test', 'mobile_app') + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=CALL_SERVICE + ) + + assert resp.status == 200 + + assert len(calls) == 1 + + +async def test_handle_fire_event(hass, mobile_app_client): + """Test that we can fire events.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_event', store_event) + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=FIRE_EVENT + ) + + assert resp.status == 200 + text = await resp.text() + assert text == "" + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' + + +async def test_update_registration(mobile_app_client, hass_client): + """Test that a we can update an existing registration via webhook.""" + mock_api_client = await hass_client() + register_resp = await mock_api_client.post( + '/api/mobile_app/devices', json=REGISTER + ) + + assert register_resp.status == 201 + register_json = await register_resp.json() + + webhook_id = register_json[CONF_WEBHOOK_ID] + + update_container = { + 'type': 'update_registration', + 'data': UPDATE + } + + update_resp = await mobile_app_client.post( + '/api/webhook/{}'.format(webhook_id), json=update_container + ) + + assert update_resp.status == 200 + update_json = await update_resp.json() + assert update_json['app_version'] == '2.0.0' + assert CONF_WEBHOOK_ID not in update_json + assert CONF_SECRET not in update_json + + +async def test_returns_error_incorrect_json(mobile_app_client, caplog): + """Test that an error is returned when JSON is invalid.""" + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + data='not json' + ) + + assert resp.status == 400 + json = await resp.json() + assert json == [] + assert 'invalid JSON' in caplog.text + + +async def test_handle_decryption(mobile_app_client): + """Test that we can encrypt/decrypt properly.""" + try: + # pylint: disable=unused-import + from nacl.secret import SecretBox # noqa: F401 + from nacl.encoding import Base64Encoder # noqa: F401 + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + keylen = SecretBox.KEY_SIZE + key = "58eb127991594dad934d1584bdee5f27".encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + + data = SecretBox(key).encrypt(payload, + encoder=Base64Encoder).decode("utf-8") + + container = { + 'type': 'render_template', + 'encrypted': True, + 'encrypted_data': data, + } + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=container + ) + + assert resp.status == 200 + + json = await resp.json() + assert json == {'rendered': 'Hello world'} + + +async def test_register_device(hass_client, mock_api_client): + """Test that a device can be registered.""" + try: + # pylint: disable=unused-import + from nacl.secret import SecretBox # noqa: F401 + from nacl.encoding import Base64Encoder # noqa: F401 + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + resp = await mock_api_client.post( + '/api/mobile_app/devices', json=REGISTER + ) + + assert resp.status == 201 + register_json = await resp.json() + assert CONF_WEBHOOK_ID in register_json + assert CONF_SECRET in register_json + + keylen = SecretBox.KEY_SIZE + key = register_json[CONF_SECRET].encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + + data = SecretBox(key).encrypt(payload, + encoder=Base64Encoder).decode("utf-8") + + container = { + 'type': 'render_template', + 'encrypted': True, + 'encrypted_data': data, + } + + mobile_app_client = await hass_client() + + resp = await mobile_app_client.post( + '/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]), + json=container + ) + + assert resp.status == 200 + + webhook_json = await resp.json() + assert webhook_json == {'rendered': 'Hello world'} From e877983533c8c6ed9ae212e35e92a80d5e07dc22 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 2 Mar 2019 08:09:12 +0100 Subject: [PATCH 17/58] Make time trigger data trigger.now local (#21544) * Make time trigger data trigger.now local * Make time pattern trigger data trigger.now local * Lint * Rework according to review comment * Lint --- homeassistant/helpers/event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c1dae00bed5..b55c259f503 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -370,7 +370,9 @@ def async_track_utc_time_change(hass, action, last_now = now if next_time <= now: - hass.async_run_job(action, event.data[ATTR_NOW]) + if local: + now = dt_util.as_local(now) + hass.async_run_job(action, now) calculate_next(now + timedelta(seconds=1)) # We can't use async_track_point_in_utc_time here because it would From 996e0a6389199e0122ce41eecdae3e885b4d3fd8 Mon Sep 17 00:00:00 2001 From: damarco Date: Fri, 1 Mar 2019 19:47:20 +0100 Subject: [PATCH 18/58] Bump zigpy-deconz (#21566) --- homeassistant/components/zha/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 96c3a30d313..cafbae13421 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -33,7 +33,7 @@ REQUIREMENTS = [ 'zigpy-homeassistant==0.3.0', 'zigpy-xbee-homeassistant==0.1.2', 'zha-quirks==0.0.6', - 'zigpy-deconz==0.1.1' + 'zigpy-deconz==0.1.2' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 79e68a873ee..be4570b5457 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1823,7 +1823,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.1.1 +zigpy-deconz==0.1.2 # homeassistant.components.zha zigpy-homeassistant==0.3.0 From a268aab2ec4c29abeec82f5e5a4fc18ef419e6f3 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 1 Mar 2019 23:09:31 -0800 Subject: [PATCH 19/58] Re-thrown exception occurred in the blocking service call (#21573) * Rethrown exception occurred in the actual service call * Fix lint and test --- .../components/websocket_api/commands.py | 9 +++- .../components/websocket_api/const.py | 1 + homeassistant/helpers/service.py | 10 +++- tests/components/deconz/test_climate.py | 14 ++++-- .../components/websocket_api/test_commands.py | 46 +++++++++++++++++++ tests/test_core.py | 42 ++++++++++++++++- 6 files changed, 114 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 34bb04cb394..33a41dc8511 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,7 +3,8 @@ import voluptuous as vol from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.exceptions import Unauthorized, ServiceNotFound +from homeassistant.exceptions import Unauthorized, ServiceNotFound, \ + HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions @@ -149,6 +150,12 @@ async def handle_call_service(hass, connection, msg): except ServiceNotFound: connection.send_message(messages.error_message( msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) + except HomeAssistantError as err: + connection.send_message(messages.error_message( + msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err))) + except Exception as err: # pylint: disable=broad-except + connection.send_message(messages.error_message( + msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err))) @callback diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index fd8f7eb7b08..01145275b31 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -9,6 +9,7 @@ MAX_PENDING_MSG = 512 ERR_ID_REUSE = 'id_reuse' ERR_INVALID_FORMAT = 'invalid_format' ERR_NOT_FOUND = 'not_found' +ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error' ERR_UNKNOWN_COMMAND = 'unknown_command' ERR_UNKNOWN_ERROR = 'unknown_error' ERR_UNAUTHORIZED = 'unauthorized' diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d2211d031f5..22138d7c2aa 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -272,7 +272,10 @@ async def entity_service_call(hass, platforms, func, call, service_name=''): ] if tasks: - await asyncio.wait(tasks) + done, pending = await asyncio.wait(tasks) + assert not pending + for future in done: + future.result() # pop exception if have async def _handle_service_platform_call(func, data, entities, context): @@ -294,4 +297,7 @@ async def _handle_service_platform_call(func, data, entities, context): tasks.append(entity.async_update_ha_state(True)) if tasks: - await asyncio.wait(tasks) + done, pending = await asyncio.wait(tasks) + assert not pending + for future in done: + future.result() # pop exception if have diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 13083594c8a..fa274f1d676 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,6 +1,8 @@ """deCONZ climate platform tests.""" from unittest.mock import Mock, patch +import asynctest + from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -43,8 +45,14 @@ ENTRY_CONFIG = { async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" from pydeconz import DeconzSession - loop = Mock() - session = Mock() + + session = Mock(put=asynctest.CoroutineMock( + return_value=Mock(status=200, + json=asynctest.CoroutineMock(), + text=asynctest.CoroutineMock(), + ) + ) + ) ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor @@ -52,7 +60,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test', config_entries.CONN_CLASS_LOCAL_PUSH) gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) + gateway.api = DeconzSession(hass.loop, session, **config_entry.data) gateway.api.config = Mock() hass.data[deconz.DOMAIN] = gateway diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 78a5bf6d57e..c9ec04c5d7e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -7,6 +7,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED ) from homeassistant.components.websocket_api import const, commands +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -66,6 +67,51 @@ async def test_call_service_not_found(hass, websocket_client): assert msg['error']['code'] == const.ERR_NOT_FOUND +async def test_call_service_error(hass, websocket_client): + """Test call service command with error.""" + @callback + def ha_error_call(_): + raise HomeAssistantError('error_message') + + hass.services.async_register('domain_test', 'ha_error', ha_error_call) + + async def unknown_error_call(_): + raise ValueError('value_error') + + hass.services.async_register( + 'domain_test', 'unknown_error', unknown_error_call) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'ha_error', + }) + + msg = await websocket_client.receive_json() + print(msg) + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'home_assistant_error' + assert msg['error']['message'] == 'error_message' + + await websocket_client.send_json({ + 'id': 6, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'unknown_error', + }) + + msg = await websocket_client.receive_json() + print(msg) + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'unknown_error' + assert msg['error']['message'] == 'value_error' + + async def test_subscribe_unsubscribe_events(hass, websocket_client): """Test subscribe/unsubscribe events command.""" init_count = sum(hass.bus.async_listeners().values()) diff --git a/tests/test_core.py b/tests/test_core.py index e2ed249f441..ef9621bdac7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -726,8 +726,7 @@ class TestServiceRegistry(unittest.TestCase): """Test registering and calling an async service.""" calls = [] - @asyncio.coroutine - def service_handler(call): + async def service_handler(call): """Service handler coroutine.""" calls.append(call) @@ -803,6 +802,45 @@ class TestServiceRegistry(unittest.TestCase): self.hass.block_till_done() assert len(calls_remove) == 0 + def test_async_service_raise_exception(self): + """Test registering and calling an async service raise exception.""" + async def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + self.services.register( + 'test_domain', 'register_calls', service_handler) + self.hass.block_till_done() + + with pytest.raises(ValueError): + assert self.services.call('test_domain', 'REGISTER_CALLS', + blocking=True) + self.hass.block_till_done() + + # Non-blocking service call never throw exception + self.services.call('test_domain', 'REGISTER_CALLS', blocking=False) + self.hass.block_till_done() + + def test_callback_service_raise_exception(self): + """Test registering and calling an callback service raise exception.""" + @ha.callback + def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + self.services.register( + 'test_domain', 'register_calls', service_handler) + self.hass.block_till_done() + + with pytest.raises(ValueError): + assert self.services.call('test_domain', 'REGISTER_CALLS', + blocking=True) + self.hass.block_till_done() + + # Non-blocking service call never throw exception + self.services.call('test_domain', 'REGISTER_CALLS', blocking=False) + self.hass.block_till_done() + class TestConfig(unittest.TestCase): """Test configuration methods.""" From ec9ccf640254e21558af750fd06903c84815e983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 2 Mar 2019 11:27:36 +0100 Subject: [PATCH 20/58] Upgrade PyXiaomiGateway library (#21582) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 5e47adc47f9..19d7aaaa30d 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.11.2'] +REQUIREMENTS = ['PyXiaomiGateway==0.12.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index be4570b5457..2ae1e667cf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -67,7 +67,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.11.2 +PyXiaomiGateway==0.12.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 From c5dad8221177a689bc8d5b0d8884a0a7766fbae0 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 3 Mar 2019 21:22:22 -0800 Subject: [PATCH 21/58] Log exception occurred in WS service call command (#21584) --- homeassistant/components/websocket_api/commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 33a41dc8511..3313971e79e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -151,9 +151,11 @@ async def handle_call_service(hass, connection, msg): connection.send_message(messages.error_message( msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) except HomeAssistantError as err: + connection.logger.exception(err) connection.send_message(messages.error_message( msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err))) except Exception as err: # pylint: disable=broad-except + connection.logger.exception(err) connection.send_message(messages.error_message( msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err))) From e6debe09e80372a2754f044a26f95c1ccf5e371b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 2 Mar 2019 12:32:18 +0100 Subject: [PATCH 22/58] Word the tplink deprecation warning more strongly (#21586) --- homeassistant/components/tplink/light.py | 2 +- homeassistant/components/tplink/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 1e31df98af5..de1a943c33a 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -34,7 +34,7 @@ def async_setup_platform(hass, config, add_entities, discovery_info=None): Deprecated. """ - _LOGGER.warning('Loading as a platform is deprecated, ' + _LOGGER.warning('Loading as a platform is no longer supported, ' 'convert to use the tplink component.') diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index efff0eb4f51..65b884169c7 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -29,7 +29,7 @@ def async_setup_platform(hass, config, add_entities, discovery_info=None): Deprecated. """ - _LOGGER.warning('Loading as a platform is deprecated, ' + _LOGGER.warning('Loading as a platform is no longer supported, ' 'convert to use the tplink component.') From d9806f759bc6836fcebca45eb4a9d453ffaf921a Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 2 Mar 2019 21:57:57 -0600 Subject: [PATCH 23/58] Handle when installed app has already been removed (#21595) --- homeassistant/components/smartthings/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index e64988b2697..64e717cbc92 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -48,10 +48,20 @@ async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry): """ from pysmartthings import SmartThings - # Delete the installed app + # Remove the installed_app, which if already removed raises a 403 error. api = SmartThings(async_get_clientsession(hass), entry.data[CONF_ACCESS_TOKEN]) - await api.delete_installed_app(entry.data[CONF_INSTALLED_APP_ID]) + installed_app_id = entry.data[CONF_INSTALLED_APP_ID] + try: + await api.delete_installed_app(installed_app_id) + except ClientResponseError as ex: + if ex.status == 403: + _LOGGER.exception("Installed app %s has already been removed", + installed_app_id) + else: + raise + _LOGGER.debug("Removed installed app %s", installed_app_id) + # Delete the entry hass.async_create_task( hass.config_entries.async_remove(entry.entry_id)) From d5bdfdb0b3b1e580c45fa3cb99ef9c83d17e4da2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 4 Mar 2019 15:55:26 -0800 Subject: [PATCH 24/58] Resolve race condition when HA auth provider is loading (#21619) * Resolve race condition when HA auth provider is loading * Fix * Add more tests * Lint --- homeassistant/auth/mfa_modules/notify.py | 20 +++++++++----- homeassistant/auth/mfa_modules/totp.py | 14 +++++++--- homeassistant/auth/providers/homeassistant.py | 17 ++++++++---- tests/auth/mfa_modules/test_notify.py | 24 +++++++++++++++++ tests/auth/mfa_modules/test_totp.py | 24 +++++++++++++++++ tests/auth/providers/test_homeassistant.py | 27 +++++++++++++++++++ 6 files changed, 110 insertions(+), 16 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 3c26f8b4bde..310abff9484 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -2,6 +2,7 @@ Sending HOTP through notify service """ +import asyncio import logging from collections import OrderedDict from typing import Any, Dict, Optional, List @@ -90,6 +91,7 @@ class NotifyAuthModule(MultiFactorAuthModule): self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) self._message_template = config[CONF_MESSAGE] + self._init_lock = asyncio.Lock() @property def input_schema(self) -> vol.Schema: @@ -98,15 +100,19 @@ class NotifyAuthModule(MultiFactorAuthModule): async def _async_load(self) -> None: """Load stored data.""" - data = await self._user_store.async_load() + async with self._init_lock: + if self._user_settings is not None: + return - if data is None: - data = {STORAGE_USERS: {}} + data = await self._user_store.async_load() - self._user_settings = { - user_id: NotifySetting(**setting) - for user_id, setting in data.get(STORAGE_USERS, {}).items() - } + if data is None: + data = {STORAGE_USERS: {}} + + self._user_settings = { + user_id: NotifySetting(**setting) + for user_id, setting in data.get(STORAGE_USERS, {}).items() + } async def _async_save(self) -> None: """Save data.""" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 68f4e1d0596..dc51152f565 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,4 +1,5 @@ """Time-based One Time Password auth module.""" +import asyncio import logging from io import BytesIO from typing import Any, Dict, Optional, Tuple # noqa: F401 @@ -68,6 +69,7 @@ class TotpAuthModule(MultiFactorAuthModule): self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True) + self._init_lock = asyncio.Lock() @property def input_schema(self) -> vol.Schema: @@ -76,12 +78,16 @@ class TotpAuthModule(MultiFactorAuthModule): async def _async_load(self) -> None: """Load stored data.""" - data = await self._user_store.async_load() + async with self._init_lock: + if self._users is not None: + return - if data is None: - data = {STORAGE_USERS: {}} + data = await self._user_store.async_load() - self._users = data.get(STORAGE_USERS, {}) + if data is None: + data = {STORAGE_USERS: {}} + + self._users = data.get(STORAGE_USERS, {}) async def _async_save(self) -> None: """Save data.""" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index b22f93f11f1..2187d272800 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -1,4 +1,5 @@ """Home Assistant auth provider.""" +import asyncio import base64 from collections import OrderedDict import logging @@ -204,15 +205,21 @@ class HassAuthProvider(AuthProvider): DEFAULT_TITLE = 'Home Assistant Local' - data = None + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize an Home Assistant auth provider.""" + super().__init__(*args, **kwargs) + self.data = None # type: Optional[Data] + self._init_lock = asyncio.Lock() async def async_initialize(self) -> None: """Initialize the auth provider.""" - if self.data is not None: - return + async with self._init_lock: + if self.data is not None: + return - self.data = Data(self.hass) - await self.data.async_load() + data = Data(self.hass) + await data.async_load() + self.data = data async def async_login_flow( self, context: Optional[Dict]) -> LoginFlow: diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 748b5507824..c0680024dae 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,4 +1,5 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" +import asyncio from unittest.mock import patch from homeassistant import data_entry_flow @@ -395,3 +396,26 @@ async def test_not_raise_exception_when_service_not_exist(hass): # wait service call finished await hass.async_block_till_done() + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the data loading.""" + counter = 0 + + async def mock_load(_): + """Mock homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + notify_auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'notify' + }) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = notify_auth_module.async_validate('user', {'code': 'value'}) + task2 = notify_auth_module.async_validate('user', {'code': 'value'}) + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert results[0] is False + assert results[1] is False diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index d400fe80672..35ab21ae6de 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,4 +1,5 @@ """Test the Time-based One Time Password (MFA) auth module.""" +import asyncio from unittest.mock import patch from homeassistant import data_entry_flow @@ -128,3 +129,26 @@ async def test_login_flow_validates_mfa(hass): result['flow_id'], {'code': MOCK_CODE}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data'].id == 'mock-user' + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the data loading.""" + counter = 0 + + async def mock_load(_): + """Mock of homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + totp_auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'totp' + }) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = totp_auth_module.async_validate('user', {'code': 'value'}) + task2 = totp_auth_module.async_validate('user', {'code': 'value'}) + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert results[0] is False + assert results[1] is False diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index ffc4d67f21d..c466a1fa42b 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,4 +1,5 @@ """Test the Home Assistant local auth provider.""" +import asyncio from unittest.mock import Mock, patch import pytest @@ -288,3 +289,29 @@ async def test_legacy_get_or_create_credentials(hass, legacy_data): 'username': 'hello ' }) assert credentials1 is not credentials3 + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the hass_auth.Data loading. + + Ref issue: https://github.com/home-assistant/home-assistant/issues/21569 + """ + counter = 0 + + async def mock_load(_): + """Mock of homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass), + {'type': 'homeassistant'}) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = provider.async_validate_login('user', 'pass') + task2 = provider.async_validate_login('user', 'pass') + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert isinstance(results[0], hass_auth.InvalidAuth) + # results[1] will be a TypeError if race condition occurred + assert isinstance(results[1], hass_auth.InvalidAuth) From 932080656df5ba9a6cfcfbae11809cc64d3f353c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 3 Mar 2019 18:49:29 +0100 Subject: [PATCH 25/58] Upgrade pysonos to 0.0.8 (#21624) --- homeassistant/components/sonos/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index bcac4ce272c..e9f297e4f07 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.7'] +REQUIREMENTS = ['pysonos==0.0.8'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 2ae1e667cf6..3d9236fbd34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1267,7 +1267,7 @@ pysmartthings==0.6.3 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.7 +pysonos==0.0.8 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0840ee8f710..6e5638d167a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ pysmartapp==0.3.0 pysmartthings==0.6.3 # homeassistant.components.sonos -pysonos==0.0.7 +pysonos==0.0.8 # homeassistant.components.spc pyspcwebgw==0.4.0 From cca8d4c9516d879df1a1e9c76ba3f85e72951ebd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Mar 2019 20:36:13 -0800 Subject: [PATCH 26/58] Fix calc next (#21630) --- homeassistant/helpers/event.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b55c259f503..5e262a47565 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -370,9 +370,7 @@ def async_track_utc_time_change(hass, action, last_now = now if next_time <= now: - if local: - now = dt_util.as_local(now) - hass.async_run_job(action, now) + hass.async_run_job(action, dt_util.as_local(now) if local else now) calculate_next(now + timedelta(seconds=1)) # We can't use async_track_point_in_utc_time here because it would From a382ba731d75f0aa3e3aa49976f0a3686d20e05b Mon Sep 17 00:00:00 2001 From: Gijs Reichert Date: Mon, 4 Mar 2019 15:25:28 +0100 Subject: [PATCH 27/58] Cast displaytime to int for JSON RPC (#21649) --- homeassistant/components/notify/kodi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 74bfe61d3f2..50d2246cd29 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -90,7 +90,7 @@ class KodiNotificationService(BaseNotificationService): try: data = kwargs.get(ATTR_DATA) or {} - displaytime = data.get(ATTR_DISPLAYTIME, 10000) + displaytime = int(data.get(ATTR_DISPLAYTIME, 10000)) icon = data.get(ATTR_ICON, "info") title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) await self._server.GUI.ShowNotification( From f5a0b5ab98a531c5f059ec3962be066cede256c1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Mar 2019 00:53:16 +0100 Subject: [PATCH 28/58] :shirt: Corrects unit of measurement symbol for Watt (#21654) --- homeassistant/components/toon/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 762374eb41c..29b58fbfff9 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -14,7 +14,7 @@ DEFAULT_MAX_TEMP = 30.0 DEFAULT_MIN_TEMP = 6.0 CURRENCY_EUR = 'EUR' -POWER_WATT = 'Watt' +POWER_WATT = 'W' POWER_KWH = 'kWh' RATIO_PERCENT = '%' VOLUME_CM3 = 'CM3' From 81c252f91745107ffcf9c96765dc8681d5fb1727 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Mar 2019 13:18:16 -0800 Subject: [PATCH 29/58] Rename Google Assistant evenets (#21655) --- homeassistant/components/google_assistant/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index b7d3a398ef2..220ed6dd58c 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -44,6 +44,6 @@ ERR_UNKNOWN_ERROR = 'unknownError' ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' # Event types -EVENT_COMMAND_RECEIVED = 'google_assistant_command_received' -EVENT_QUERY_RECEIVED = 'google_assistant_query_received' -EVENT_SYNC_RECEIVED = 'google_assistant_sync_received' +EVENT_COMMAND_RECEIVED = 'google_assistant_command' +EVENT_QUERY_RECEIVED = 'google_assistant_query' +EVENT_SYNC_RECEIVED = 'google_assistant_sync' From 31b88197eb65e5d406bd56c127907752d44ce2a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Mar 2019 00:52:00 +0100 Subject: [PATCH 30/58] :ambulance: Fixes Toon doing I/O in coroutines (#21657) --- homeassistant/components/toon/__init__.py | 10 ++++++---- .../components/toon/binary_sensor.py | 2 +- homeassistant/components/toon/climate.py | 2 +- homeassistant/components/toon/config_flow.py | 20 +++++++++---------- homeassistant/components/toon/sensor.py | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index fce0bc4ed2a..00006fd7c17 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,6 +1,7 @@ """Support for Toon van Eneco devices.""" import logging from typing import Any, Dict +from functools import partial import voluptuous as vol @@ -48,10 +49,11 @@ async def async_setup_entry(hass: HomeAssistantType, conf = hass.data.get(DATA_TOON_CONFIG) - toon = Toon(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], - conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], - tenant_id=entry.data[CONF_TENANT], - display_common_name=entry.data[CONF_DISPLAY]) + toon = await hass.async_add_executor_job(partial( + Toon, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], + conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], + tenant_id=entry.data[CONF_TENANT], + display_common_name=entry.data[CONF_DISPLAY])) hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 891a72daeed..a50a67085ec 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -102,7 +102,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorDevice): return value - async def async_update(self) -> None: + def update(self) -> None: """Get the latest data from the binary sensor.""" section = getattr(self.toon, self.section) self._state = getattr(section, self.measurement) diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 2e564b8457a..13f1c1269a1 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -117,7 +117,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice): """Set new operation mode.""" self.toon.thermostat_state = HA_TOON[operation_mode] - async def async_update(self) -> None: + def update(self) -> None: """Update local state.""" if self.toon.thermostat_state is None: self._state = None diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index cdb8a0f2257..a09b3dd49a7 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Toon component.""" from collections import OrderedDict import logging +from functools import partial import voluptuous as vol @@ -75,11 +76,10 @@ class ToonFlowHandler(config_entries.ConfigFlow): app = self.hass.data.get(DATA_TOON_CONFIG, {}) try: - toon = Toon(user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - app[CONF_CLIENT_ID], - app[CONF_CLIENT_SECRET], - tenant_id=user_input[CONF_TENANT]) + toon = await self.hass.async_add_executor_job(partial( + Toon, user_input[CONF_USERNAME], user_input[CONF_PASSWORD], + app[CONF_CLIENT_ID], app[CONF_CLIENT_SECRET], + tenant_id=user_input[CONF_TENANT])) displays = toon.display_names @@ -136,12 +136,10 @@ class ToonFlowHandler(config_entries.ConfigFlow): app = self.hass.data.get(DATA_TOON_CONFIG, {}) try: - Toon(self.username, - self.password, - app[CONF_CLIENT_ID], - app[CONF_CLIENT_SECRET], - tenant_id=self.tenant, - display_common_name=user_input[CONF_DISPLAY]) + await self.hass.async_add_executor_job(partial( + Toon, self.username, self.password, app[CONF_CLIENT_ID], + app[CONF_CLIENT_SECRET], tenant_id=self.tenant, + display_common_name=user_input[CONF_DISPLAY])) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error while authenticating") diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 2a5921b78eb..e263bda9fc7 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -134,7 +134,7 @@ class ToonSensor(ToonEntity): """Return the unit this state is expressed in.""" return self._unit_of_measurement - async def async_update(self) -> None: + def update(self) -> None: """Get the latest data from the sensor.""" section = getattr(self.toon, self.section) value = None From a778cd117f6165837a59906803efd4fea96cc198 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Mar 2019 00:51:15 +0100 Subject: [PATCH 31/58] Upgrade toonapilib to 3.1.0 (#21661) --- homeassistant/components/toon/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 00006fd7c17..12cae9ac801 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -16,7 +16,7 @@ from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) -REQUIREMENTS = ['toonapilib==3.0.9'] +REQUIREMENTS = ['toonapilib==3.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3d9236fbd34..c3784acacf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1689,7 +1689,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonapilib==3.0.9 +toonapilib==3.1.0 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e5638d167a..d404e18a06f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -295,7 +295,7 @@ srpenergy==1.0.5 statsd==3.2.1 # homeassistant.components.toon -toonapilib==3.0.9 +toonapilib==3.1.0 # homeassistant.components.camera.uvc uvcclient==0.11.0 From b20b811cb936d387518b1bdc611b1239be2c78a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Mar 2019 16:01:31 -0800 Subject: [PATCH 32/58] Avoid recorder thread crashing (#21668) --- homeassistant/components/recorder/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index e0af36ea409..6c338457b34 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -318,6 +318,10 @@ class Recorder(threading.Thread): CONNECT_RETRY_WAIT) tries += 1 + except exc.SQLAlchemyError: + updated = True + _LOGGER.exception("Error saving event: %s", event) + if not updated: _LOGGER.error("Error in database update. Could not save " "after %d tries. Giving up", tries) From 3135257c0dc17e5eef2a2354d20ba07d365abec9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Mar 2019 16:02:05 -0800 Subject: [PATCH 33/58] Bumped version to 0.89.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a3dfff9a0b0..6e68c2f8e04 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 2303e1684ef208bbacece90ebde01ea07d9d64a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Mar 2019 11:45:31 -0800 Subject: [PATCH 34/58] Updated frontend to 20190305.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index fbbea13f026..d7c1aabdb49 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190303.0'] +REQUIREMENTS = ['home-assistant-frontend==20190305.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index c3784acacf9..088800d3c4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190303.0 +home-assistant-frontend==20190305.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d404e18a06f..154d6442d25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,7 +120,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190303.0 +home-assistant-frontend==20190305.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 4978a1681ead511127e6170d26f0fbd002fd8fce Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 5 Mar 2019 05:18:25 +0000 Subject: [PATCH 35/58] check we have a tb (#21670) --- homeassistant/components/system_log/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 16786bdeba4..d6877c32f0d 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -91,15 +91,15 @@ class LogEntry: self.first_occured = self.timestamp = record.created self.level = record.levelname self.message = record.getMessage() + self.exception = '' + self.root_cause = None if record.exc_info: self.exception = ''.join( traceback.format_exception(*record.exc_info)) _, _, tb = record.exc_info # pylint: disable=invalid-name # Last line of traceback contains the root cause of the exception - self.root_cause = str(traceback.extract_tb(tb)[-1]) - else: - self.exception = '' - self.root_cause = None + if traceback.extract_tb(tb): + self.root_cause = str(traceback.extract_tb(tb)[-1]) self.source = source self.count = 1 From cb613984dfb5094412f39128e400da657701c9b8 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Tue, 5 Mar 2019 11:07:40 +0100 Subject: [PATCH 36/58] Fix ADS race condition (#21677) --- homeassistant/components/ads/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 060e9b2b987..1b90e645af4 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -171,13 +171,12 @@ class AdsHub: hnotify, huser = self._client.add_device_notification( name, attr, self._device_notification_callback) hnotify = int(hnotify) + self._notification_items[hnotify] = NotificationItem( + hnotify, huser, name, plc_datatype, callback) _LOGGER.debug( "Added device notification %d for variable %s", hnotify, name) - self._notification_items[hnotify] = NotificationItem( - hnotify, huser, name, plc_datatype, callback) - def _device_notification_callback(self, notification, name): """Handle device notifications.""" contents = notification.contents @@ -187,9 +186,10 @@ class AdsHub: data = contents.data try: - notification_item = self._notification_items[hnotify] + with self._lock: + notification_item = self._notification_items[hnotify] except KeyError: - _LOGGER.debug("Unknown device notification handle: %d", hnotify) + _LOGGER.error("Unknown device notification handle: %d", hnotify) return # Parse data to desired datatype From 4c72f3c48b05c3a9a3e3f6fa575efeddbd1156b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Mar 2019 11:46:30 -0800 Subject: [PATCH 37/58] Bumped version to 0.89.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6e68c2f8e04..31f7d11734c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c2f4293c6a45309eac2ccfcbdeb1126aa0c6e96a Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 6 Mar 2019 13:52:25 +0100 Subject: [PATCH 38/58] resync hass that changes have occured (#21705) --- homeassistant/components/tellduslive/cover.py | 3 +++ homeassistant/components/tellduslive/light.py | 1 + homeassistant/components/tellduslive/switch.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 5a22311d7f0..1bd3158d100 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -44,11 +44,14 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice): def close_cover(self, **kwargs): """Close the cover.""" self.device.down() + self._update_callback() def open_cover(self, **kwargs): """Open the cover.""" self.device.up() + self._update_callback() def stop_cover(self, **kwargs): """Stop the cover.""" self.device.stop() + self._update_callback() diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 10eaee1ad8b..12baf8384f6 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -45,6 +45,7 @@ class TelldusLiveLight(TelldusLiveEntity, Light): def changed(self): """Define a property of the device that might have changed.""" self._last_brightness = self.brightness + self._update_callback() @property def brightness(self): diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index 63d1512698c..bb0164b10bb 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -44,7 +44,9 @@ class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity): def turn_on(self, **kwargs): """Turn the switch on.""" self.device.turn_on() + self._update_callback() def turn_off(self, **kwargs): """Turn the switch off.""" self.device.turn_off() + self._update_callback() From 87b5faa2447ddc6add95b021baf300305efa1a16 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Mar 2019 16:40:29 +0100 Subject: [PATCH 39/58] Upgrade toonapilib to 3.2.1 (#21706) --- homeassistant/components/toon/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 12cae9ac801..0ca0a414fa5 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -16,7 +16,7 @@ from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) -REQUIREMENTS = ['toonapilib==3.1.0'] +REQUIREMENTS = ['toonapilib==3.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 088800d3c4d..72185c594cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1689,7 +1689,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonapilib==3.1.0 +toonapilib==3.2.1 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 154d6442d25..33a88ac4391 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -295,7 +295,7 @@ srpenergy==1.0.5 statsd==3.2.1 # homeassistant.components.toon -toonapilib==3.1.0 +toonapilib==3.2.1 # homeassistant.components.camera.uvc uvcclient==0.11.0 From 21de636e5b78531c09b0672bf667e93374674a60 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Mar 2019 10:07:31 -0800 Subject: [PATCH 40/58] Bumped version to 0.89.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 31f7d11734c..5b943ddb3cf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 279470613caf6824e5ebf856086a678482c6153f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Mar 2019 10:54:56 -0800 Subject: [PATCH 41/58] Updated frontend to 20190305.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d7c1aabdb49..0e674740269 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190305.0'] +REQUIREMENTS = ['home-assistant-frontend==20190305.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 72185c594cd..e424a433206 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190305.0 +home-assistant-frontend==20190305.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33a88ac4391..7f69e2a500a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,7 +120,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190305.0 +home-assistant-frontend==20190305.1 # homeassistant.components.homekit_controller homekit==0.12.2 From 48c9758cf546fded2f673b1ea88d8bb2d38d2e0b Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 2 Mar 2019 05:28:44 -0500 Subject: [PATCH 42/58] Upgrade blinkpy==0.13.1 (Fixes #21559) (#21578) * Upgrade blinkpy with new api endpoint * Change wifi units to dBm --- homeassistant/components/blink/__init__.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 8e95f967396..488209e3689 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['blinkpy==0.12.1'] +REQUIREMENTS = ['blinkpy==0.13.1'] _LOGGER = logging.getLogger(__name__) @@ -44,7 +44,7 @@ BINARY_SENSORS = { SENSORS = { TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'], - TYPE_WIFI_STRENGTH: ['Wifi Signal', 'bars', 'mdi:wifi-strength-2'], + TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'], } BINARY_SENSOR_SCHEMA = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index e424a433206..bc87da912de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ bellows-homeassistant==0.7.1 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.12.1 +blinkpy==0.13.1 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From aebe6ab70c4e2cbcbf63fe0e5f02c6278b06660e Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Thu, 7 Mar 2019 11:07:32 +0100 Subject: [PATCH 43/58] Fix Name of Homematic IP accesspoint in devices, if name is configured (#21617) * Fix Name of Accesspoint if name is configured * fix lint * Simplyfied naming * applied suggestion Co-Authored-By: SukramJ * update comment --- homeassistant/components/homematicip_cloud/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index fd07356d7fb..6f785565661 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -60,11 +60,14 @@ async def async_setup_entry(hass, entry): # Register hap as device in registry. device_registry = await dr.async_get_registry(hass) home = hap.home + # Add the HAP name from configuration if set. + hapname = home.label \ + if not home.name else "{} {}".format(home.label, home.name) device_registry.async_get_or_create( config_entry_id=home.id, identifiers={(DOMAIN, home.id)}, manufacturer='eQ-3', - name=home.label, + name=hapname, model=home.modelType, sw_version=home.currentAPVersion, ) From 5a555102b9958f6a0be0aa3b349b254b0f1a1bd7 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Thu, 7 Mar 2019 11:12:03 +0100 Subject: [PATCH 44/58] Fix group-switch availability for Homematic IP (#21640) * Add available=True to groups * Added unreach to stateattributes * Fixed comments * added missing sabotage check * added missing lowBat check * fix typo * apply suggestion Co-Authored-By: SukramJ * apply suggestion Co-Authored-By: SukramJ * applied suggiestions * readded lost str() * fix comment --- .../homematicip_cloud/binary_sensor.py | 45 ++++++++++++------- .../components/homematicip_cloud/device.py | 1 + .../components/homematicip_cloud/switch.py | 23 +++++++++- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4b82a500bde..d6ce4152001 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -4,6 +4,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.homematicip_cloud import ( DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) +from homeassistant.components.homematicip_cloud.device import ( + ATTR_GROUP_MEMBER_UNREACHABLE) DEPENDENCIES = ['homematicip_cloud'] @@ -31,8 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncWaterSensor, AsyncRotaryHandleSensor, AsyncMotionDetectorPushButton) - from homematicip.group import ( - SecurityGroup, SecurityZoneGroup) + from homematicip.aio.group import ( + AsyncSecurityGroup, AsyncSecurityZoneGroup) home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -48,9 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices.append(HomematicipWaterDetector(home, device)) for group in home.groups: - if isinstance(group, SecurityGroup): + if isinstance(group, AsyncSecurityGroup): devices.append(HomematicipSecuritySensorGroup(home, group)) - elif isinstance(group, SecurityZoneGroup): + elif isinstance(group, AsyncSecurityZoneGroup): devices.append(HomematicipSecurityZoneSensorGroup(home, group)) if devices: @@ -137,27 +139,37 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, """Return the class of this sensor.""" return 'safety' + @property + def available(self): + """Security-Group available.""" + # A security-group must be available, and should not be affected by + # the individual availability of group members. + return True + @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" attr = super().device_state_attributes if self._device.motionDetected: - attr.update({ATTR_MOTIONDETECTED: True}) + attr[ATTR_MOTIONDETECTED] = True if self._device.presenceDetected: - attr.update({ATTR_PRESENCEDETECTED: True}) + attr[ATTR_PRESENCEDETECTED] = True from homematicip.base.enums import WindowState if self._device.windowState is not None and \ self._device.windowState != WindowState.CLOSED: - attr.update({ATTR_WINDOWSTATE: str(self._device.windowState)}) - + attr[ATTR_WINDOWSTATE] = str(self._device.windowState) + if self._device.unreach: + attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True return attr @property def is_on(self): """Return true if security issue detected.""" if self._device.motionDetected or \ - self._device.presenceDetected: + self._device.presenceDetected or \ + self._device.unreach or \ + self._device.sabotage: return True from homematicip.base.enums import WindowState if self._device.windowState is not None and \ @@ -180,29 +192,30 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, attr = super().device_state_attributes if self._device.powerMainsFailure: - attr.update({ATTR_POWERMAINSFAILURE: True}) + attr[ATTR_POWERMAINSFAILURE] = True if self._device.moistureDetected: - attr.update({ATTR_MOISTUREDETECTED: True}) + attr[ATTR_MOISTUREDETECTED] = True if self._device.waterlevelDetected: - attr.update({ATTR_WATERLEVELDETECTED: True}) + attr[ATTR_WATERLEVELDETECTED] = True from homematicip.base.enums import SmokeDetectorAlarmType if self._device.smokeDetectorAlarmType is not None and \ self._device.smokeDetectorAlarmType != \ SmokeDetectorAlarmType.IDLE_OFF: - attr.update({ATTR_SMOKEDETECTORALARM: str( - self._device.smokeDetectorAlarmType)}) + attr[ATTR_SMOKEDETECTORALARM] = \ + str(self._device.smokeDetectorAlarmType) return attr @property def is_on(self): - """Return true if security issue detected.""" + """Return true if safety issue detected.""" parent_is_on = super().is_on from homematicip.base.enums import SmokeDetectorAlarmType if parent_is_on or \ self._device.powerMainsFailure or \ self._device.moistureDetected or \ - self._device.waterlevelDetected: + self._device.waterlevelDetected or \ + self._device.lowBat: return True if self._device.smokeDetectorAlarmType is not None and \ self._device.smokeDetectorAlarmType != \ diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 85cc3c0c77a..9940e6960db 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -21,6 +21,7 @@ ATTR_OPERATION_LOCK = 'operation_lock' ATTR_SABOTAGE = 'sabotage' ATTR_STATUS_UPDATE = 'status_update' ATTR_UNREACHABLE = 'unreachable' +ATTR_GROUP_MEMBER_UNREACHABLE = 'group_member_unreachable' class HomematicipGenericDevice(Entity): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index f129febb5e7..74f50f87b25 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -3,6 +3,8 @@ import logging from homeassistant.components.homematicip_cloud import ( DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) +from homeassistant.components.homematicip_cloud.device import ( + ATTR_GROUP_MEMBER_UNREACHABLE) from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['homematicip_cloud'] @@ -30,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncOpenCollector8Module, ) - from homematicip.group import SwitchingGroup + from homematicip.aio.group import AsyncSwitchingGroup home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -50,7 +52,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices.append(HomematicipMultiSwitch(home, device, channel)) for group in home.groups: - if isinstance(group, SwitchingGroup): + if isinstance(group, AsyncSwitchingGroup): devices.append( HomematicipGroupSwitch(home, group)) @@ -92,6 +94,23 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): """Return true if group is on.""" return self._device.on + @property + def available(self): + """Switch-Group available.""" + # A switch-group must be available, and should not be affected by the + # individual availability of group members. + # This allows switching even when individual group members + # are not available. + return True + + @property + def device_state_attributes(self): + """Return the state attributes of the switch-group.""" + attr = {} + if self._device.unreach: + attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True + return attr + async def async_turn_on(self, **kwargs): """Turn the group on.""" await self._device.turn_on() From 44341a958a2662c939608213f7803084fed39886 Mon Sep 17 00:00:00 2001 From: Leonardo Merza Date: Thu, 7 Mar 2019 14:03:32 -0500 Subject: [PATCH 45/58] automated commit 07/03/2019 10:47:38 (#21749) --- homeassistant/components/sensor/google_travel_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index 1f4d8425d6e..86b1a7aff44 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -67,7 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ })) }) -TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone', 'person'] DATA_KEY = 'google_travel_time' From e41231719426dfeaf3c8bc5ddfe46de480cba0cc Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 7 Mar 2019 11:03:02 -0800 Subject: [PATCH 46/58] Fix botvac connected maps call as it is not a supported model (#21752) --- homeassistant/components/neato/vacuum.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index ff78a087de8..990c79552b4 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -186,10 +186,11 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._battery_level = self._state['details']['charge'] if self._robot_has_map: - robot_map_id = self._robot_maps[self._robot_serial][0]['id'] + if self._state['availableServices']['maps'] != "basic-1": + robot_map_id = self._robot_maps[self._robot_serial][0]['id'] - self._robot_boundaries = self.robot.get_map_boundaries( - robot_map_id).json() + self._robot_boundaries = self.robot.get_map_boundaries( + robot_map_id).json() @property def name(self): From 5f0c37ccfc626b9c6e7c8f810230da520f6cc0e0 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 7 Mar 2019 11:07:07 -0800 Subject: [PATCH 47/58] Fix colorlog import error (#21754) * Fix colorlog import error * Lint --- homeassistant/scripts/check_config.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 1b8c6719395..cae937102cc 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -41,15 +41,20 @@ ERROR_STR = 'General Errors' def color(the_color, *args, reset=None): """Color helper.""" - from colorlog.escape_codes import escape_codes, parse_colors try: - if not args: - assert reset is None, "You cannot reset if nothing being printed" - return parse_colors(the_color) - return parse_colors(the_color) + ' '.join(args) + \ - escape_codes[reset or 'reset'] - except KeyError as k: - raise ValueError("Invalid color {} in {}".format(str(k), the_color)) + from colorlog.escape_codes import escape_codes, parse_colors + try: + if not args: + assert reset is None, "Cannot reset if nothing being printed" + return parse_colors(the_color) + return parse_colors(the_color) + ' '.join(args) + \ + escape_codes[reset or 'reset'] + except KeyError as k: + raise ValueError( + "Invalid color {} in {}".format(str(k), the_color)) + except ImportError: + # We should fallback to black-and-white if colorlog is not installed + return ' '.join(args) def run(script_args: List) -> int: From c031fd41641213889e9e5cbdf3612d487349a439 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 7 Mar 2019 16:48:14 -0800 Subject: [PATCH 48/58] Fix script load module issue (#21763) * Fix script load depedency * Revert #21754 --- homeassistant/scripts/__init__.py | 9 ++------- homeassistant/scripts/check_config.py | 21 ++++++++------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 3050379a496..070d907a7d9 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -52,15 +52,10 @@ def run(args: List) -> int: hass = HomeAssistant(loop) pkgload = PackageLoadable(hass) for req in getattr(script, 'REQUIREMENTS', []): - try: - loop.run_until_complete(pkgload.loadable(req)) + if loop.run_until_complete(pkgload.loadable(req)): continue - except ImportError: - pass - returncode = install_package(req, **_pip_kwargs) - - if not returncode: + if not install_package(req, **_pip_kwargs): print('Aborting script, could not install dependency', req) return 1 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index cae937102cc..1b8c6719395 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -41,20 +41,15 @@ ERROR_STR = 'General Errors' def color(the_color, *args, reset=None): """Color helper.""" + from colorlog.escape_codes import escape_codes, parse_colors try: - from colorlog.escape_codes import escape_codes, parse_colors - try: - if not args: - assert reset is None, "Cannot reset if nothing being printed" - return parse_colors(the_color) - return parse_colors(the_color) + ' '.join(args) + \ - escape_codes[reset or 'reset'] - except KeyError as k: - raise ValueError( - "Invalid color {} in {}".format(str(k), the_color)) - except ImportError: - # We should fallback to black-and-white if colorlog is not installed - return ' '.join(args) + if not args: + assert reset is None, "You cannot reset if nothing being printed" + return parse_colors(the_color) + return parse_colors(the_color) + ' '.join(args) + \ + escape_codes[reset or 'reset'] + except KeyError as k: + raise ValueError("Invalid color {} in {}".format(str(k), the_color)) def run(script_args: List) -> int: From 1638d0a92fb4d893771654c5b6c7865696897861 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 7 Mar 2019 22:55:11 +0100 Subject: [PATCH 49/58] Bump PyXiaomiGateway version to 0.12.2 (Closes: #21731) (#21764) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 19d7aaaa30d..66fc1fa13dd 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.12.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.12.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bc87da912de..03713b7e168 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -67,7 +67,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.12.0 +PyXiaomiGateway==0.12.2 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 From 4d6f21ecb27dfc2f2a6a5ff99c751edd109c60e9 Mon Sep 17 00:00:00 2001 From: David Thulke Date: Fri, 8 Mar 2019 01:46:50 +0100 Subject: [PATCH 50/58] adds missing SUPPORT_VOLUME_SET flag to webos media_player (#21766) --- homeassistant/components/webostv/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index a6cbfbae99d..35c3c456680 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_CUSTOMIZE, CONF_FILENAME, CONF_HOST, CONF_NAME, CONF_TIMEOUT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) @@ -36,7 +36,7 @@ WEBOSTV_CONFIG_FILE = 'webostv.conf' SUPPORT_WEBOSTV = SUPPORT_TURN_OFF | \ SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) From a121c92f52749983161907c1c354ce781315f6c4 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 7 Mar 2019 19:46:01 -0500 Subject: [PATCH 51/58] Updated to newest pyeconet (#21772) --- homeassistant/components/water_heater/econet.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/water_heater/econet.py b/homeassistant/components/water_heater/econet.py index 69fde44bdd2..efc21798859 100644 --- a/homeassistant/components/water_heater/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -13,7 +13,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyeconet==0.0.8'] +REQUIREMENTS = ['pyeconet==0.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 03713b7e168..765bf4d0e35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ pydukeenergy==0.0.6 pyebox==1.1.4 # homeassistant.components.water_heater.econet -pyeconet==0.0.8 +pyeconet==0.0.9 # homeassistant.components.switch.edimax pyedimax==0.1 From eae6d1c7a686781b095bf24872107fdbf6b58a25 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Mar 2019 16:48:53 -0800 Subject: [PATCH 52/58] Bumped version to 0.89.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5b943ddb3cf..c1ec2da41c7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From f3e8e34089bbe74f1a336a840bb2a26633b04d9e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Mar 2019 17:03:23 -0800 Subject: [PATCH 53/58] Add workflow for tests --- .github/main.workflow | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/main.workflow diff --git a/.github/main.workflow b/.github/main.workflow new file mode 100644 index 00000000000..54869682e1c --- /dev/null +++ b/.github/main.workflow @@ -0,0 +1,41 @@ +workflow "Python 3.7 - tox" { + resolves = ["Python 3.7 - tests"] + on = "push" +} + +action "Python 3.7 - tests" { + uses = "home-assistant/actions/py37-tox@master" + args = "-e py37" +} + +workflow "Python 3.6 - tox" { + resolves = ["Python 3.6 - tests"] + on = "push" +} + +action "Python 3.6 - tests" { + uses = "home-assistant/actions/py36-tox@master" + args = "-e py36" +} + +workflow "Python 3.5 - tox" { + resolves = ["Pyton 3.5 - typing"] + on = "push" +} + +action "Python 3.5 - tests" { + uses = "home-assistant/actions/py35-tox@master" + args = "-e py35" +} + +action "Python 3.5 - lints" { + uses = "home-assistant/actions/py35-tox@master" + needs = ["Python 3.5 - tests"] + args = "-e lint" +} + +action "Pyton 3.5 - typing" { + uses = "home-assistant/actions/py35-tox@master" + args = "-e typing" + needs = ["Python 3.5 - lints"] +} From 39749952eeaa08b6d5546e97541c5cf6a6e01ccc Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 8 Mar 2019 23:20:07 -0600 Subject: [PATCH 54/58] Update dependencies to receive data on webhook callbacks (#21838) --- homeassistant/components/smartthings/__init__.py | 9 ++++++--- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/smartthings/conftest.py | 3 ++- tests/components/smartthings/test_init.py | 16 ++++++++++++---- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 64e717cbc92..88035e3cc79 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -27,7 +27,7 @@ from .smartapp import ( setup_smartapp, setup_smartapp_endpoint, smartapp_sync_subscriptions, validate_installed_app) -REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.3'] +REQUIREMENTS = ['pysmartapp==0.3.1', 'pysmartthings==0.6.7'] DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) @@ -290,7 +290,8 @@ class DeviceBroker: if not device: continue device.status.apply_attribute_update( - evt.component_id, evt.capability, evt.attribute, evt.value) + evt.component_id, evt.capability, evt.attribute, evt.value, + data=evt.data) # Fire events for buttons if evt.capability == Capability.button and \ @@ -300,7 +301,8 @@ class DeviceBroker: 'device_id': evt.device_id, 'location_id': evt.location_id, 'value': evt.value, - 'name': device.label + 'name': device.label, + 'data': evt.data } self._hass.bus.async_fire(EVENT_BUTTON, data) _LOGGER.debug("Fired button event: %s", data) @@ -312,6 +314,7 @@ class DeviceBroker: 'capability': evt.capability, 'attribute': evt.attribute, 'value': evt.value, + 'data': evt.data } _LOGGER.debug("Push update received: %s", data) diff --git a/requirements_all.txt b/requirements_all.txt index 765bf4d0e35..aa1f78ab701 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,10 +1256,10 @@ pysher==1.0.1 pysma==0.3.1 # homeassistant.components.smartthings -pysmartapp==0.3.0 +pysmartapp==0.3.1 # homeassistant.components.smartthings -pysmartthings==0.6.3 +pysmartthings==0.6.7 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f69e2a500a..073768d0cf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,10 +224,10 @@ pyps4-homeassistant==0.3.0 pyqwikswitch==0.8 # homeassistant.components.smartthings -pysmartapp==0.3.0 +pysmartapp==0.3.1 # homeassistant.components.smartthings -pysmartthings==0.6.3 +pysmartthings==0.6.7 # homeassistant.components.sonos pysonos==0.0.8 diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 27e833bff25..67c35ba8232 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -326,7 +326,7 @@ def scene_fixture(scene_factory): def event_factory_fixture(): """Fixture for creating mock devices.""" def _factory(device_id, event_type="DEVICE_EVENT", capability='', - attribute='Updated', value='Value'): + attribute='Updated', value='Value', data=None): event = Mock() event.event_type = event_type event.device_id = device_id @@ -334,6 +334,7 @@ def event_factory_fixture(): event.capability = capability event.attribute = attribute event.value = value + event.data = data event.location_id = str(uuid4()) return event return _factory diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index ec0b3982517..1f648c7716a 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -235,16 +235,21 @@ async def test_broker_regenerates_token( async def test_event_handler_dispatches_updated_devices( - hass, config_entry, device_factory, event_request_factory): + hass, config_entry, device_factory, event_request_factory, + event_factory): """Test the event handler dispatches updated devices.""" devices = [ device_factory('Bedroom 1 Switch', ['switch']), device_factory('Bathroom 1', ['switch']), device_factory('Sensor', ['motionSensor']), + device_factory('Lock', ['lock']) ] device_ids = [devices[0].device_id, devices[1].device_id, - devices[2].device_id] - request = event_request_factory(device_ids) + devices[2].device_id, devices[3].device_id] + event = event_factory(devices[3].device_id, capability='lock', + attribute='lock', value='locked', + data={'codeId': '1'}) + request = event_request_factory(device_ids=device_ids, events=[event]) config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id called = False @@ -265,6 +270,8 @@ async def test_event_handler_dispatches_updated_devices( assert called for device in devices: assert device.status.values['Updated'] == 'Value' + assert devices[3].status.attributes['lock'].value == 'locked' + assert devices[3].status.attributes['lock'].data == {'codeId': '1'} async def test_event_handler_ignores_other_installed_app( @@ -308,7 +315,8 @@ async def test_event_handler_fires_button_events( 'device_id': device.device_id, 'location_id': event.location_id, 'value': 'pushed', - 'name': device.label + 'name': device.label, + 'data': None } hass.bus.async_listen(EVENT_BUTTON, handler) broker = smartthings.DeviceBroker( From 1b4905ae5a5b1df36a854b8cba91838a101fae13 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 8 Mar 2019 23:56:37 -0800 Subject: [PATCH 55/58] Override http.trusted_networks by auth_provider.trusted_networks (#21844) --- homeassistant/components/http/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4928ae2ab17..93afbc04396 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -211,6 +211,14 @@ class HomeAssistantHTTP: "legacy_api_password support has been enabled. If you don't " "require it, remove the 'api_password' from your http config.") + for prv in hass.auth.auth_providers: + if prv.type == 'trusted_networks': + # auth_provider.trusted_networks will override + # http.trusted_networks, http.trusted_networks will be + # removed from future release + trusted_networks = prv.trusted_networks + break + setup_auth(app, trusted_networks, api_password if hass.auth.support_legacy else None) From 00d01865cf0b06100d119ae94eaf7ede9906a15d Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sat, 9 Mar 2019 14:06:35 -0800 Subject: [PATCH 56/58] Fix botvac when no map exists (#21877) --- homeassistant/components/neato/vacuum.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 990c79552b4..2f2f3904947 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -187,10 +187,12 @@ class NeatoConnectedVacuum(StateVacuumDevice): if self._robot_has_map: if self._state['availableServices']['maps'] != "basic-1": - robot_map_id = self._robot_maps[self._robot_serial][0]['id'] + if self._robot_maps[self._robot_serial]: + robot_map_id = ( + self._robot_maps[self._robot_serial][0]['id']) - self._robot_boundaries = self.robot.get_map_boundaries( - robot_map_id).json() + self._robot_boundaries = self.robot.get_map_boundaries( + robot_map_id).json() @property def name(self): From 1f3e4c5776d1d2b05b6f0934f1d7aa0b3bc729ef Mon Sep 17 00:00:00 2001 From: Klaudiusz Staniek Date: Mon, 11 Mar 2019 21:27:41 +0100 Subject: [PATCH 57/58] Fixes issues #21821 and #21819 (#21911) * Fix #21821 * datetime fix * local time to utc conversion fix * Test cases update * date import removed * Update tod.py --- homeassistant/components/binary_sensor/tod.py | 20 ++++--- tests/components/binary_sensor/test_tod.py | 59 ++++++++++--------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/binary_sensor/tod.py b/homeassistant/components/binary_sensor/tod.py index 7dc6e5ebe81..8e29bbd4678 100644 --- a/homeassistant/components/binary_sensor/tod.py +++ b/homeassistant/components/binary_sensor/tod.py @@ -119,6 +119,17 @@ class TodSensor(BinarySensorDevice): self.hass.config.time_zone).isoformat(), } + def _naive_time_to_utc_datetime(self, naive_time): + """Convert naive time from config to utc_datetime with current day.""" + # get the current local date from utc time + current_local_date = self.current_datetime.astimezone( + self.hass.config.time_zone).date() + # calcuate utc datetime corecponding to local time + utc_datetime = self.hass.config.time_zone.localize( + datetime.combine( + current_local_date, naive_time)).astimezone(tz=pytz.UTC) + return utc_datetime + def _calculate_initial_boudary_time(self): """Calculate internal absolute time boudaries.""" nowutc = self.current_datetime @@ -134,9 +145,7 @@ class TodSensor(BinarySensorDevice): # datetime.combine(date, time, tzinfo) is not supported # in python 3.5. The self._after is provided # with hass configured TZ not system wide - after_event_date = datetime.combine( - nowutc, self._after.replace( - tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + after_event_date = self._naive_time_to_utc_datetime(self._after) self._time_after = after_event_date @@ -154,9 +163,7 @@ class TodSensor(BinarySensorDevice): self.hass, self._before, after_event_date) else: # Convert local time provided to UTC today, see above - before_event_date = datetime.combine( - nowutc, self._before.replace( - tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + before_event_date = self._naive_time_to_utc_datetime(self._before) # It is safe to add timedelta days=1 to UTC as there is no DST if before_event_date < after_event_date + self._after_offset: @@ -190,7 +197,6 @@ class TodSensor(BinarySensorDevice): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - await super().async_added_to_hass() self._calculate_initial_boudary_time() self._calculate_next_update() self._point_in_time_listener(dt_util.now()) diff --git a/tests/components/binary_sensor/test_tod.py b/tests/components/binary_sensor/test_tod.py index 3c083141962..7af6ef95bfa 100644 --- a/tests/components/binary_sensor/test_tod.py +++ b/tests/components/binary_sensor/test_tod.py @@ -110,8 +110,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_midnight_turnover_after_midnight_inside_period(self): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime( - 2019, 1, 10, 21, 00, 0, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 10, 21, 0, 0)).astimezone(pytz.UTC) config = { 'binary_sensor': [ { @@ -143,8 +143,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_midnight_turnover_before_midnight_outside_period(self): """Test midnight turnover setting before midnight outside period.""" - test_time = datetime( - 2019, 1, 10, 20, 30, 0, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 10, 20, 30, 0)).astimezone(pytz.UTC) config = { 'binary_sensor': [ { @@ -165,8 +165,9 @@ class TestBinarySensorTod(unittest.TestCase): def test_midnight_turnover_after_midnight_outside_period(self): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime( - 2019, 1, 10, 20, 0, 0, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 10, 20, 0, 0)).astimezone(pytz.UTC) + config = { 'binary_sensor': [ { @@ -185,8 +186,8 @@ class TestBinarySensorTod(unittest.TestCase): state = self.hass.states.get('binary_sensor.night') assert state.state == STATE_OFF - switchover_time = datetime( - 2019, 1, 11, 4, 59, 0, tzinfo=self.hass.config.time_zone) + switchover_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 11, 4, 59, 0)).astimezone(pytz.UTC) with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', return_value=switchover_time): @@ -210,8 +211,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_from_sunrise_to_sunset(self): """Test period from sunrise to sunset.""" - test_time = datetime( - 2019, 1, 12, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 12)).astimezone(pytz.UTC) sunrise = dt_util.as_local(get_astral_event_date( self.hass, 'sunrise', dt_util.as_utc(test_time))) sunset = dt_util.as_local(get_astral_event_date( @@ -299,8 +300,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_from_sunset_to_sunrise(self): """Test period from sunset to sunrise.""" - test_time = datetime( - 2019, 1, 12, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 12)).astimezone(pytz.UTC) sunset = dt_util.as_local(get_astral_event_date( self.hass, 'sunset', test_time)) sunrise = dt_util.as_local(get_astral_event_next( @@ -385,14 +386,14 @@ class TestBinarySensorTod(unittest.TestCase): def test_offset(self): """Test offset.""" - after = datetime( - 2019, 1, 10, 18, 0, 0, - tzinfo=self.hass.config.time_zone) + \ + after = self.hass.config.time_zone.localize( + datetime(2019, 1, 10, 18, 0, 0)).astimezone(pytz.UTC) + \ timedelta(hours=1, minutes=34) - before = datetime( - 2019, 1, 10, 22, 0, 0, - tzinfo=self.hass.config.time_zone) + \ + + before = self.hass.config.time_zone.localize( + datetime(2019, 1, 10, 22, 0, 0)).astimezone(pytz.UTC) + \ timedelta(hours=1, minutes=45) + entity_id = 'binary_sensor.evening' config = { 'binary_sensor': [ @@ -457,9 +458,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_offset_overnight(self): """Test offset overnight.""" - after = datetime( - 2019, 1, 10, 18, 0, 0, - tzinfo=self.hass.config.time_zone) + \ + after = self.hass.config.time_zone.localize( + datetime(2019, 1, 10, 18, 0, 0)).astimezone(pytz.UTC) + \ timedelta(hours=1, minutes=34) entity_id = 'binary_sensor.evening' config = { @@ -498,7 +498,8 @@ class TestBinarySensorTod(unittest.TestCase): self.hass.config.latitude = 69.6 self.hass.config.longitude = 18.8 - test_time = datetime(2010, 1, 1, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2010, 1, 1)).astimezone(pytz.UTC) sunrise = dt_util.as_local(get_astral_event_next( self.hass, 'sunrise', dt_util.as_utc(test_time))) sunset = dt_util.as_local(get_astral_event_next( @@ -600,13 +601,13 @@ class TestBinarySensorTod(unittest.TestCase): self.hass.config.latitude = 69.6 self.hass.config.longitude = 18.8 - test_time = datetime(2010, 6, 1, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2010, 6, 1)).astimezone(pytz.UTC) + sunrise = dt_util.as_local(get_astral_event_next( self.hass, 'sunrise', dt_util.as_utc(test_time))) sunset = dt_util.as_local(get_astral_event_next( self.hass, 'sunset', dt_util.as_utc(test_time))) - print(sunrise) - print(sunset) config = { 'binary_sensor': [ { @@ -701,8 +702,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_sun_offset(self): """Test sun event with offset.""" - test_time = datetime( - 2019, 1, 12, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 1, 12)).astimezone(pytz.UTC) sunrise = dt_util.as_local(get_astral_event_date( self.hass, 'sunrise', dt_util.as_utc(test_time)) + timedelta(hours=-1, minutes=-30)) @@ -810,8 +811,8 @@ class TestBinarySensorTod(unittest.TestCase): def test_dst(self): """Test sun event with offset.""" self.hass.config.time_zone = pytz.timezone('CET') - test_time = datetime( - 2019, 3, 30, 3, 0, 0, tzinfo=self.hass.config.time_zone) + test_time = self.hass.config.time_zone.localize( + datetime(2019, 3, 30, 3, 0, 0)).astimezone(pytz.UTC) config = { 'binary_sensor': [ { From 737c7e871dc811dea7865f86cd71ea1c41c7bda9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Mar 2019 11:51:09 -0700 Subject: [PATCH 58/58] Bumped version to 0.89.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c1ec2da41c7..a740bdd441e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)