From 81b95f4050e7b3c0eeb80026e0270b34fba9ab6b Mon Sep 17 00:00:00 2001 From: Dubh Ad Date: Wed, 22 Jul 2020 20:48:48 +0100 Subject: [PATCH 01/51] Update discord.py to v1.3.4 for API change (#38060) --- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 7f03e52ca62..1f4ccbdf5f5 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,6 +2,6 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.3.3"], + "requirements": ["discord.py==1.3.4"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 2492a229806..064edd5ba08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -473,7 +473,7 @@ directv==0.3.0 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.3.3 +discord.py==1.3.4 # homeassistant.components.updater distro==1.5.0 From 80b0c10a38e5fe062d393807e8a15d2f79b2293e Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 23 Jul 2020 00:09:37 +0200 Subject: [PATCH 02/51] Fix issue with creation of PT2262 devices in rfxtrx integration (#38074) --- homeassistant/components/rfxtrx/__init__.py | 4 ++-- homeassistant/components/rfxtrx/cover.py | 5 ++++- homeassistant/components/rfxtrx/light.py | 5 ++++- homeassistant/components/rfxtrx/sensor.py | 5 +++-- homeassistant/components/rfxtrx/switch.py | 5 ++++- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 1f4655aedc7..576e38316c6 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -194,7 +194,7 @@ def setup_internal(hass, config): "sub_type": event.device.subtype, "type_string": event.device.type_string, "id_string": event.device.id_string, - "data": "".join(f"{x:02x}" for x in event.data), + "data": binascii.hexlify(event.data).decode("ASCII"), "values": getattr(event, "values", None), } @@ -339,7 +339,7 @@ def get_device_id(device, data_bits=None): if data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: masked_id = get_pt2262_deviceid(id_string, data_bits) if masked_id: - id_string = str(masked_id) + id_string = masked_id.decode("ASCII") return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index af5c48810ee..41df8d022ec 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -7,6 +7,7 @@ from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, + CONF_DATA_BITS, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, @@ -38,7 +39,9 @@ async def async_setup_entry( if not supported(event): continue - device_id = get_device_id(event.device) + device_id = get_device_id( + event.device, data_bits=entity_info.get(CONF_DATA_BITS) + ) if device_id in device_ids: continue device_ids.add(device_id) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 71bf54d3d50..3e3ef95fbf7 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -13,6 +13,7 @@ from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, + CONF_DATA_BITS, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, SIGNAL_EVENT, @@ -50,7 +51,9 @@ async def async_setup_entry( if not supported(event): continue - device_id = get_device_id(event.device) + device_id = get_device_id( + event.device, data_bits=entity_info.get(CONF_DATA_BITS) + ) if device_id in device_ids: continue device_ids.add(device_id) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 537fabd7aa7..fb7176d2f91 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -14,6 +14,7 @@ from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, + CONF_DATA_BITS, DATA_TYPES, SIGNAL_EVENT, RfxtrxEntity, @@ -64,7 +65,7 @@ async def async_setup_entry( return isinstance(event, (ControlEvent, SensorEvent)) entities = [] - for packet_id in discovery_info[CONF_DEVICES]: + for packet_id, entity in discovery_info[CONF_DEVICES].items(): event = get_rfx_object(packet_id) if event is None: _LOGGER.error("Invalid device: %s", packet_id) @@ -72,7 +73,7 @@ async def async_setup_entry( if not supported(event): continue - device_id = get_device_id(event.device) + device_id = get_device_id(event.device, data_bits=entity.get(CONF_DATA_BITS)) for data_type in set(event.values) & set(DATA_TYPES): data_id = (*device_id, data_type) if data_id in data_ids: diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index e5c96215c83..6cd9a484abd 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -9,6 +9,7 @@ from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, + CONF_DATA_BITS, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, DOMAIN, @@ -48,7 +49,9 @@ async def async_setup_entry( if not supported(event): continue - device_id = get_device_id(event.device) + device_id = get_device_id( + event.device, data_bits=entity_info.get(CONF_DATA_BITS) + ) if device_id in device_ids: continue device_ids.add(device_id) From 49f59007df91c3effff8409a5b52bc394900d098 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Jul 2020 21:43:51 -0700 Subject: [PATCH 03/51] Fix route53 depending on broken package (#38079) --- homeassistant/components/route53/__init__.py | 10 +++------- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 3 --- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index cda4ba9dc86..1656ca393bf 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -4,7 +4,7 @@ import logging from typing import List import boto3 -from ipify import exceptions, get_ip +import requests import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE, HTTP_OK @@ -84,16 +84,12 @@ def _update_route53( # Get the IP Address and build an array of changes try: - ipaddress = get_ip() + ipaddress = requests.get("https://api.ipify.org/", timeout=5).text() - except exceptions.ConnectionError: + except requests.RequestException: _LOGGER.warning("Unable to reach the ipify service") return - except exceptions.ServiceError: - _LOGGER.warning("Unable to complete the ipfy request") - return - changes = [] for record in records: _LOGGER.debug("Processing record: %s", record) diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index da2b6dc092c..4879f12a3be 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -2,6 +2,6 @@ "domain": "route53", "name": "AWS Route53", "documentation": "https://www.home-assistant.io/integrations/route53", - "requirements": ["boto3==1.9.252", "ipify==1.0.0"], + "requirements": ["boto3==1.9.252"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 064edd5ba08..f9cafb4c5a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,9 +783,6 @@ influxdb==5.2.3 # homeassistant.components.iperf3 iperf3==0.1.11 -# homeassistant.components.route53 -ipify==1.0.0 - # homeassistant.components.rest # homeassistant.components.verisure jsonpath==0.82 From 71048545fa0760ab9975a79515ac9bf464d225d8 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Wed, 22 Jul 2020 18:01:57 -0500 Subject: [PATCH 04/51] Bump pysmartthings to v0.7.2 (#38086) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smartthings/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 3f9ee75b173..bf137ae398d 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,7 +3,7 @@ "name": "SmartThings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.1"], + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.2"], "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@andrewsayre"] diff --git a/requirements_all.txt b/requirements_all.txt index f9cafb4c5a0..68d38258da4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1605,7 +1605,7 @@ pysmappee==0.1.5 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.7.1 +pysmartthings==0.7.2 # homeassistant.components.smarty pysmarty==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02595d9f708..7fbc85a7036 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -737,7 +737,7 @@ pysmappee==0.1.5 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.7.1 +pysmartthings==0.7.2 # homeassistant.components.soma pysoma==0.0.10 diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index a7ca9a4744c..643c084720a 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -249,7 +249,7 @@ def subscription_factory_fixture(): def device_factory_fixture(): """Fixture for creating mock devices.""" api = Mock(Api) - api.post_device_command.return_value = {} + api.post_device_command.return_value = {"results": [{"status": "ACCEPTED"}]} def _factory(label, capabilities, status: dict = None): device_data = { From 7ad75493ff15186cc25e64b00702da874c8f95f2 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 23 Jul 2020 14:18:46 -0700 Subject: [PATCH 05/51] Bump androidtv to 0.0.46 (#38090) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index d1747b8cd42..40e7575bbb9 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.0", - "androidtv[async]==0.0.45", + "androidtv[async]==0.0.46", "pure-python-adb==0.2.2.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 68d38258da4..2a8639f35b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.45 +androidtv[async]==0.0.46 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fbc85a7036..9ed7a520895 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -132,7 +132,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.45 +androidtv[async]==0.0.46 # homeassistant.components.apns apns2==0.3.0 From 3e7ada20568f1fb3fccc333924397651acce163a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jul 2020 20:21:57 -1000 Subject: [PATCH 06/51] Prevent the zeroconf service browser from terminating when a device without any addresses is discovered. (#38094) --- homeassistant/components/zeroconf/__init__.py | 8 +++++++ tests/components/zeroconf/test_init.py | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 7534ad39541..3c448a91010 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -209,6 +209,11 @@ def setup(hass, config): return info = info_from_service(service_info) + if not info: + # Prevent the browser thread from collapsing + _LOGGER.debug("Failed to get addresses for device %s", name) + return + _LOGGER.debug("Discovered new device %s %s", name, info) # If we can handle it as a HomeKit discovery, we do that here. @@ -310,6 +315,9 @@ def info_from_service(service): except UnicodeDecodeError: pass + if not service.addresses: + return None + address = service.addresses[0] info = { diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 412e1f5f3f5..0e4d9aa904c 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -49,6 +49,20 @@ def get_service_info_mock(service_type, name): ) +def get_service_info_mock_without_an_address(service_type, name): + """Return service info for get_service_info without any addresses.""" + return ServiceInfo( + service_type, + name, + addresses=[], + port=80, + weight=0, + priority=0, + server="name.local.", + properties=PROPERTIES, + ) + + def get_homekit_info_mock(model, pairing_status): """Return homekit info for get_service_info for an homekit device.""" @@ -286,6 +300,15 @@ async def test_info_from_service_non_utf8(hass): assert raw_info["non-utf8-value"] is NON_UTF8_VALUE +async def test_info_from_service_with_addresses(hass): + """Test info_from_service does not throw when there are no addresses.""" + service_type = "_test._tcp.local." + info = zeroconf.info_from_service( + get_service_info_mock_without_an_address(service_type, f"test.{service_type}") + ) + assert info is None + + async def test_get_instance(hass, mock_zeroconf): """Test we get an instance.""" assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf From 7fe5fee12481c18789bb3b3c769a86bfed748beb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 23 Jul 2020 20:02:29 -0600 Subject: [PATCH 07/51] Fix SimpliSafe to work with new MFA (#38097) * Fix SimpliSafe to work with new MFA * Code review (part 1) * Input needed from Martin * Code review * Code review * Restore YAML * Tests * Code review * Remove JSON patching in tests * Add reauth test * One more reauth test * Don't abuse the word "conf" * Update homeassistant/components/simplisafe/config_flow.py Co-authored-by: Martin Hjelmare * Test coverage Co-authored-by: Martin Hjelmare --- .../components/simplisafe/__init__.py | 77 +++++---- .../simplisafe/alarm_control_panel.py | 12 +- .../components/simplisafe/config_flow.py | 132 +++++++++++++--- homeassistant/components/simplisafe/const.py | 3 + homeassistant/components/simplisafe/lock.py | 10 +- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/strings.json | 20 ++- .../simplisafe/translations/en.json | 22 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/simplisafe/test_config_flow.py | 148 +++++++++++++----- 11 files changed, 314 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 8895244158a..327549eeb62 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,6 +1,6 @@ """Support for SimpliSafe alarm systems.""" import asyncio -import logging +from uuid import UUID from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError @@ -55,11 +55,10 @@ from .const import ( DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, + LOGGER, VOLUMES, ) -_LOGGER = logging.getLogger(__name__) - CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" @@ -161,6 +160,13 @@ def _async_save_refresh_token(hass, config_entry, token): ) +async def async_get_client_id(hass): + """Get a client ID (based on the HASS unique ID) for the SimpliSafe API.""" + hass_id = await hass.helpers.instance_id.async_get() + # SimpliSafe requires full, "dashed" versions of UUIDs: + return str(UUID(hass_id)) + + async def async_register_base_station(hass, system, config_entry_id): """Register a new bridge.""" device_registry = await dr.async_get_registry(hass) @@ -220,17 +226,18 @@ async def async_setup_entry(hass, config_entry): _verify_domain_control = verify_domain_control(hass, DOMAIN) + client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) try: api = await API.login_via_token( - config_entry.data[CONF_TOKEN], session=websession + config_entry.data[CONF_TOKEN], client_id=client_id, session=websession ) except InvalidCredentialsError: - _LOGGER.error("Invalid credentials provided") + LOGGER.error("Invalid credentials provided") return False except SimplipyError as err: - _LOGGER.error("Config entry failed: %s", err) + LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady _async_save_refresh_token(hass, config_entry, api.refresh_token) @@ -252,7 +259,7 @@ async def async_setup_entry(hass, config_entry): """Decorate.""" system_id = int(call.data[ATTR_SYSTEM_ID]) if system_id not in simplisafe.systems: - _LOGGER.error("Unknown system ID in service call: %s", system_id) + LOGGER.error("Unknown system ID in service call: %s", system_id) return await coro(call) @@ -266,7 +273,7 @@ async def async_setup_entry(hass, config_entry): """Decorate.""" system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])] if system.version != 3: - _LOGGER.error("Service only available on V3 systems") + LOGGER.error("Service only available on V3 systems") return await coro(call) @@ -280,7 +287,7 @@ async def async_setup_entry(hass, config_entry): try: await system.clear_notifications() except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) + LOGGER.error("Error during service call: %s", err) return @verify_system_exists @@ -291,7 +298,7 @@ async def async_setup_entry(hass, config_entry): try: await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) + LOGGER.error("Error during service call: %s", err) return @verify_system_exists @@ -302,7 +309,7 @@ async def async_setup_entry(hass, config_entry): try: await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) + LOGGER.error("Error during service call: %s", err) return @verify_system_exists @@ -320,7 +327,7 @@ async def async_setup_entry(hass, config_entry): } ) except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) + LOGGER.error("Error during service call: %s", err) return for service, method, schema in [ @@ -373,16 +380,16 @@ class SimpliSafeWebsocket: @staticmethod def _on_connect(): """Define a handler to fire when the websocket is connected.""" - _LOGGER.info("Connected to websocket") + LOGGER.info("Connected to websocket") @staticmethod def _on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" - _LOGGER.info("Disconnected from websocket") + LOGGER.info("Disconnected from websocket") def _on_event(self, event): """Define a handler to fire when a new SimpliSafe event arrives.""" - _LOGGER.debug("New websocket event: %s", event) + LOGGER.debug("New websocket event: %s", event) async_dispatcher_send( self._hass, TOPIC_UPDATE_WEBSOCKET.format(event.system_id), event ) @@ -451,7 +458,7 @@ class SimpliSafe: if not to_add: return - _LOGGER.debug("New system notifications: %s", to_add) + LOGGER.debug("New system notifications: %s", to_add) self._system_notifications[system.system_id].update(to_add) @@ -492,7 +499,7 @@ class SimpliSafe: system.system_id ] = await system.get_latest_event() except SimplipyError as err: - _LOGGER.error("Error while fetching initial event: %s", err) + LOGGER.error("Error while fetching initial event: %s", err) self.initial_event_to_use[system.system_id] = {} async def refresh(event_time): @@ -512,7 +519,7 @@ class SimpliSafe: """Update a system.""" await system.update() self._async_process_new_notifications(system) - _LOGGER.debug('Updated REST API data for "%s"', system.address) + LOGGER.debug('Updated REST API data for "%s"', system.address) async_dispatcher_send( self._hass, TOPIC_UPDATE_REST_API.format(system.system_id) ) @@ -523,27 +530,37 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): if self._emergency_refresh_token_used: - _LOGGER.error( - "SimpliSafe authentication disconnected. Please restart HASS" + LOGGER.error( + "Token disconnected or invalid. Please re-auth the " + "SimpliSafe integration in HASS" ) - remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop( - self._config_entry.entry_id + self._hass.async_create_task( + self._hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=self._config_entry.data, + ) ) - remove_listener() return - _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") + LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") self._emergency_refresh_token_used = True - return await self._api.refresh_access_token( - self._config_entry.data[CONF_TOKEN] - ) + + try: + await self._api.refresh_access_token( + self._config_entry.data[CONF_TOKEN] + ) + return + except SimplipyError as err: + LOGGER.error("Error while using stored refresh token: %s", err) + return if isinstance(result, SimplipyError): - _LOGGER.error("SimpliSafe error while updating: %s", result) + LOGGER.error("SimpliSafe error while updating: %s", result) return - if isinstance(result, SimplipyError): - _LOGGER.error("Unknown error while updating: %s", result) + if isinstance(result, Exception): # pylint: disable=broad-except + LOGGER.error("Unknown error while updating: %s", result) return if self._api.refresh_token != self._config_entry.data[CONF_TOKEN]: diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 7998de463f6..acc7e3bb0a8 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,5 +1,4 @@ """Support for SimpliSafe alarm control panels.""" -import logging import re from simplipy.errors import SimplipyError @@ -50,11 +49,10 @@ from .const import ( ATTR_VOICE_PROMPT_VOLUME, DATA_CLIENT, DOMAIN, + LOGGER, VOLUME_STRING_MAP, ) -_LOGGER = logging.getLogger(__name__) - ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" ATTR_GSM_STRENGTH = "gsm_strength" ATTR_PIN_NAME = "pin_name" @@ -146,7 +144,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return True if not code or code != self._simplisafe.options[CONF_CODE]: - _LOGGER.warning( + LOGGER.warning( "Incorrect alarm code entered (target state: %s): %s", state, code ) return False @@ -161,7 +159,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): try: await self._system.set_off() except SimplipyError as err: - _LOGGER.error('Error while disarming "%s": %s', self._system.name, err) + LOGGER.error('Error while disarming "%s": %s', self._system.name, err) return self._state = STATE_ALARM_DISARMED @@ -174,7 +172,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): try: await self._system.set_home() except SimplipyError as err: - _LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err) + LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err) return self._state = STATE_ALARM_ARMED_HOME @@ -187,7 +185,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): try: await self._system.set_away() except SimplipyError as err: - _LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err) + LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err) return self._state = STATE_ALARM_ARMING diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 1225f6de818..d4a076860a1 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,6 +1,10 @@ """Config flow to configure the SimpliSafe component.""" from simplipy import API -from simplipy.errors import SimplipyError +from simplipy.errors import ( + InvalidCredentialsError, + PendingAuthorizationError, + SimplipyError, +) import voluptuous as vol from homeassistant import config_entries @@ -8,7 +12,8 @@ from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERN from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint: disable=unused-import +from . import async_get_client_id +from .const import DOMAIN, LOGGER # pylint: disable=unused-import class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -19,21 +24,18 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.data_schema = vol.Schema( + self.full_data_schema = vol.Schema( { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_CODE): str, } ) + self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str}) - async def _show_form(self, errors=None): - """Show the form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=self.data_schema, - errors=errors if errors else {}, - ) + self._code = None + self._password = None + self._username = None @staticmethod @callback @@ -41,34 +43,112 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) + async def _async_get_simplisafe_api(self): + """Get an authenticated SimpliSafe API client.""" + client_id = await async_get_client_id(self.hass) + websession = aiohttp_client.async_get_clientsession(self.hass) + + return await API.login_via_credentials( + self._username, self._password, client_id=client_id, session=websession, + ) + + async def _async_login_during_step(self, *, step_id, form_schema): + """Attempt to log into the API from within a config flow step.""" + errors = {} + + try: + simplisafe = await self._async_get_simplisafe_api() + except PendingAuthorizationError: + LOGGER.info("Awaiting confirmation of MFA email click") + return await self.async_step_mfa() + except InvalidCredentialsError: + errors = {"base": "invalid_credentials"} + except SimplipyError as err: + LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) + errors = {"base": "unknown"} + + if errors: + return self.async_show_form( + step_id=step_id, data_schema=form_schema, errors=errors, + ) + + return await self.async_step_finish( + { + CONF_USERNAME: self._username, + CONF_TOKEN: simplisafe.refresh_token, + CONF_CODE: self._code, + } + ) + + async def async_step_finish(self, user_input=None): + """Handle finish config entry setup.""" + existing_entry = await self.async_set_unique_id(self._username) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self._username, data=user_input) + async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) + async def async_step_mfa(self, user_input=None): + """Handle multi-factor auth confirmation.""" + if user_input is None: + return self.async_show_form(step_id="mfa") + + try: + simplisafe = await self._async_get_simplisafe_api() + except PendingAuthorizationError: + LOGGER.error("Still awaiting confirmation of MFA email click") + return self.async_show_form( + step_id="mfa", errors={"base": "still_awaiting_mfa"} + ) + + return await self.async_step_finish( + { + CONF_USERNAME: self._username, + CONF_TOKEN: simplisafe.refresh_token, + CONF_CODE: self._code, + } + ) + + async def async_step_reauth(self, config): + """Handle configuration by re-auth.""" + self._code = config.get(CONF_CODE) + self._username = config[CONF_USERNAME] + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", data_schema=self.password_data_schema + ) + + self._password = user_input[CONF_PASSWORD] + + return await self._async_login_during_step( + step_id="reauth_confirm", form_schema=self.password_data_schema + ) + async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" if not user_input: - return await self._show_form() + return self.async_show_form( + step_id="user", data_schema=self.full_data_schema + ) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() - websession = aiohttp_client.async_get_clientsession(self.hass) + self._code = user_input.get(CONF_CODE) + self._password = user_input[CONF_PASSWORD] + self._username = user_input[CONF_USERNAME] - try: - simplisafe = await API.login_via_credentials( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=websession - ) - except SimplipyError: - return await self._show_form(errors={"base": "invalid_credentials"}) - - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data={ - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_TOKEN: simplisafe.refresh_token, - CONF_CODE: user_input.get(CONF_CODE), - }, + return await self._async_login_during_step( + step_id="user", form_schema=self.full_data_schema ) diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index 6ca5f8323a7..36d191d0ab8 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -1,8 +1,11 @@ """Define constants for the SimpliSafe component.""" from datetime import timedelta +import logging from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +LOGGER = logging.getLogger(__package__) + DOMAIN = "simplisafe" DATA_CLIENT = "client" diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 78866ce9004..82177fb4387 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,6 +1,4 @@ """Support for SimpliSafe locks.""" -import logging - from simplipy.errors import SimplipyError from simplipy.lock import LockStates from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED @@ -9,9 +7,7 @@ from homeassistant.components.lock import LockEntity from homeassistant.core import callback from . import SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_CLIENT, DOMAIN, LOGGER ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_JAMMED = "jammed" @@ -52,7 +48,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): try: await self._lock.lock() except SimplipyError as err: - _LOGGER.error('Error while locking "%s": %s', self._lock.name, err) + LOGGER.error('Error while locking "%s": %s', self._lock.name, err) return self._is_locked = True @@ -62,7 +58,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): try: await self._lock.unlock() except SimplipyError as err: - _LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) + LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) return self._is_locked = False diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 6b271012c8e..c986add4539 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.2.0"], + "requirements": ["simplisafe-python==9.2.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 0a097c9fda8..7f724de9db5 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,6 +1,17 @@ { "config": { "step": { + "mfa": { + "title": "SimpliSafe Multi-Factor Authentication", + "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration." + }, + "reauth_confirm": { + "title": "Re-link SimpliSafe Account", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "title": "Fill in your information.", "data": { @@ -12,10 +23,13 @@ }, "error": { "identifier_exists": "Account already registered", - "invalid_credentials": "Invalid credentials" + "invalid_credentials": "Invalid credentials", + "still_awaiting_mfa": "Still awaiting MFA email click", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "This SimpliSafe account is already in use." + "already_configured": "This SimpliSafe account is already in use.", + "reauth_successful": "SimpliSafe successfully reauthenticated." } }, "options": { @@ -28,4 +42,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 90867a0163f..29ad4ee88ef 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -1,18 +1,32 @@ { "config": { "abort": { - "already_configured": "This SimpliSafe account is already in use." + "already_configured": "This SimpliSafe account is already in use.", + "reauth_successful": "SimpliSafe successfully reauthenticated." }, "error": { "identifier_exists": "Account already registered", - "invalid_credentials": "Invalid credentials" + "invalid_credentials": "Invalid credentials", + "still_awaiting_mfa": "Still awaiting MFA email click", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "mfa": { + "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", + "title": "SimpliSafe Multi-Factor Authentication" + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "title": "Re-link SimpliSafe Account" + }, "user": { "data": { "code": "Code (used in Home Assistant UI)", - "password": "Password", - "username": "Email" + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::email%]" }, "title": "Fill in your information." } diff --git a/requirements_all.txt b/requirements_all.txt index 2a8639f35b2..3ff7a8ff322 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.2.0 +simplisafe-python==9.2.1 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ed7a520895..bc48e7f7e49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -857,7 +857,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.2.0 +simplisafe-python==9.2.1 # homeassistant.components.sleepiq sleepyq==0.7 diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 2448b20b084..d5e22a48d8a 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,14 +1,16 @@ """Define tests for the SimpliSafe config flow.""" -import json - -from simplipy.errors import SimplipyError +from simplipy.errors import ( + InvalidCredentialsError, + PendingAuthorizationError, + SimplipyError, +) from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from tests.async_mock import MagicMock, PropertyMock, mock_open, patch +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry @@ -21,11 +23,17 @@ def mock_api(): async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" - conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + } - MockConfigEntry(domain=DOMAIN, unique_id="user@email.com", data=conf).add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, + unique_id="user@email.com", + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -39,8 +47,9 @@ async def test_invalid_credentials(hass): """Test that invalid credentials throws an error.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + print("AARON") with patch( - "simplipy.API.login_via_credentials", side_effect=SimplipyError, + "simplipy.API.login_via_credentials", side_effect=InvalidCredentialsError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -75,15 +84,12 @@ async def test_options_flow(hass): async def test_show_form(hass): """Test that the form is served with no input.""" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" async def test_step_import(hass): @@ -94,17 +100,9 @@ async def test_step_import(hass): CONF_CODE: "1234", } - mop = mock_open(read_data=json.dumps({"refresh_token": "12345"})) - with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch( - "homeassistant.util.json.open", mop, create=True - ), patch( - "homeassistant.util.json.os.open", return_value=0 - ), patch( - "homeassistant.util.json.os.replace" - ): + ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) @@ -118,25 +116,48 @@ async def test_step_import(hass): } +async def test_step_reauth(hass): + """Test that the reauth step works.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="user@email.com", + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data={CONF_CODE: "1234", CONF_USERNAME: "user@email.com"}, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + + async def test_step_user(hass): - """Test that the user step works.""" + """Test that the user step works (without MFA).""" conf = { CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", CONF_CODE: "1234", } - mop = mock_open(read_data=json.dumps({"refresh_token": "12345"})) - with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch( - "homeassistant.util.json.open", mop, create=True - ), patch( - "homeassistant.util.json.os.open", return_value=0 - ), patch( - "homeassistant.util.json.os.replace" - ): + ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) @@ -148,3 +169,58 @@ async def test_step_user(hass): CONF_TOKEN: "12345abc", CONF_CODE: "1234", } + + +async def test_step_user_mfa(hass): + """Test that the user step works when MFA is in the middle.""" + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + } + + with patch( + "simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["step_id"] == "mfa" + + with patch( + "simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError + ): + # Simulate the user pressing the MFA submit button without having clicked + # the link in the MFA email: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["step_id"] == "mfa" + + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_TOKEN: "12345abc", + CONF_CODE: "1234", + } + + +async def test_unknown_error(hass): + """Test that an unknown error raises the correct error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + with patch( + "simplipy.API.login_via_credentials", side_effect=SimplipyError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["errors"] == {"base": "unknown"} From bf8bfa63210cfe57e61a0bc89039ee07a7cfbf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 23 Jul 2020 08:20:49 +0200 Subject: [PATCH 08/51] Fix text error when getting getting external IP in route53 (#38100) --- homeassistant/components/route53/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 1656ca393bf..5355ed15f38 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -84,7 +84,7 @@ def _update_route53( # Get the IP Address and build an array of changes try: - ipaddress = requests.get("https://api.ipify.org/", timeout=5).text() + ipaddress = requests.get("https://api.ipify.org/", timeout=5).text except requests.RequestException: _LOGGER.warning("Unable to reach the ipify service") From 24b8545ebe2a9a49f72883af6390b44b58e27413 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 24 Jul 2020 01:11:21 -0500 Subject: [PATCH 09/51] Fix script repeat variable lifetime (#38124) --- homeassistant/helpers/script.py | 44 +++++++----- tests/helpers/test_script.py | 116 ++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1ca13e22e9f..1eb46a1eded 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -140,7 +140,7 @@ class _ScriptRun: ) -> None: self._hass = hass self._script = script - self._variables = variables + self._variables = variables or {} self._context = context self._log_exceptions = log_exceptions self._step = -1 @@ -431,22 +431,23 @@ class _ScriptRun: async def _async_repeat_step(self): """Repeat a sequence.""" - description = self._action.get(CONF_ALIAS, "sequence") repeat = self._action[CONF_REPEAT] - async def async_run_sequence(iteration, extra_msg="", extra_vars=None): + saved_repeat_vars = self._variables.get("repeat") + + def set_repeat_var(iteration, count=None): + repeat_vars = {"first": iteration == 1, "index": iteration} + if count: + repeat_vars["last"] = iteration == count + self._variables["repeat"] = repeat_vars + + # pylint: disable=protected-access + script = self._script._get_repeat_script(self._step) + + async def async_run_sequence(iteration, extra_msg=""): self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg) - repeat_vars = {"repeat": {"first": iteration == 1, "index": iteration}} - if extra_vars: - repeat_vars["repeat"].update(extra_vars) - # pylint: disable=protected-access - await self._async_run_script( - self._script._get_repeat_script(self._step), - # Add repeat to variables. Override if it already exists in case of - # nested calls. - {**(self._variables or {}), **repeat_vars}, - ) + await self._async_run_script(script) if CONF_COUNT in repeat: count = repeat[CONF_COUNT] @@ -461,10 +462,10 @@ class _ScriptRun: level=logging.ERROR, ) raise _StopScript + extra_msg = f" of {count}" for iteration in range(1, count + 1): - await async_run_sequence( - iteration, f" of {count}", {"last": iteration == count} - ) + set_repeat_var(iteration, count) + await async_run_sequence(iteration, extra_msg) if self._stop.is_set(): break @@ -473,6 +474,7 @@ class _ScriptRun: await self._async_get_condition(config) for config in repeat[CONF_WHILE] ] for iteration in itertools.count(1): + set_repeat_var(iteration) if self._stop.is_set() or not all( cond(self._hass, self._variables) for cond in conditions ): @@ -484,12 +486,18 @@ class _ScriptRun: await self._async_get_condition(config) for config in repeat[CONF_UNTIL] ] for iteration in itertools.count(1): + set_repeat_var(iteration) await async_run_sequence(iteration) if self._stop.is_set() or all( cond(self._hass, self._variables) for cond in conditions ): break + if saved_repeat_vars: + self._variables["repeat"] = saved_repeat_vars + else: + del self._variables["repeat"] + async def _async_choose_step(self): """Choose a sequence.""" # pylint: disable=protected-access @@ -503,11 +511,11 @@ class _ScriptRun: if choose_data["default"]: await self._async_run_script(choose_data["default"]) - async def _async_run_script(self, script, variables=None): + async def _async_run_script(self, script): """Execute a script.""" await self._async_run_long_action( self._hass.async_create_task( - script.async_run(variables or self._variables, self._context) + script.async_run(self._variables, self._context) ) ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8a27c1c4e7e..7a458c49286 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -854,6 +854,122 @@ async def test_repeat_conditional(hass, condition): assert event.data.get("index") == str(index + 1) +@pytest.mark.parametrize("condition", ["while", "until"]) +async def test_repeat_var_in_condition(hass, condition): + """Test repeat action w/ while option.""" + event = "test_event" + events = async_capture_events(hass, event) + + sequence = {"repeat": {"sequence": {"event": event}}} + if condition == "while": + sequence["repeat"]["while"] = { + "condition": "template", + "value_template": "{{ repeat.index <= 2 }}", + } + else: + sequence["repeat"]["until"] = { + "condition": "template", + "value_template": "{{ repeat.index == 2 }}", + } + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA(sequence)) + + with mock.patch( + "homeassistant.helpers.condition._LOGGER.error", + side_effect=AssertionError("Template Error"), + ): + await script_obj.async_run() + + assert len(events) == 2 + + +async def test_repeat_nested(hass): + """Test nested repeats.""" + event = "test_event" + events = async_capture_events(hass, event) + + sequence = cv.SCRIPT_SCHEMA( + [ + { + "event": event, + "event_data_template": { + "repeat": "{{ None if repeat is not defined else repeat }}" + }, + }, + { + "repeat": { + "count": 2, + "sequence": [ + { + "event": event, + "event_data_template": { + "first": "{{ repeat.first }}", + "index": "{{ repeat.index }}", + "last": "{{ repeat.last }}", + }, + }, + { + "repeat": { + "count": 2, + "sequence": { + "event": event, + "event_data_template": { + "first": "{{ repeat.first }}", + "index": "{{ repeat.index }}", + "last": "{{ repeat.last }}", + }, + }, + } + }, + { + "event": event, + "event_data_template": { + "first": "{{ repeat.first }}", + "index": "{{ repeat.index }}", + "last": "{{ repeat.last }}", + }, + }, + ], + } + }, + { + "event": event, + "event_data_template": { + "repeat": "{{ None if repeat is not defined else repeat }}" + }, + }, + ] + ) + script_obj = script.Script(hass, sequence, "test script") + + with mock.patch( + "homeassistant.helpers.condition._LOGGER.error", + side_effect=AssertionError("Template Error"), + ): + await script_obj.async_run() + + assert len(events) == 10 + assert events[0].data == {"repeat": "None"} + assert events[-1].data == {"repeat": "None"} + for index, result in enumerate( + ( + ("True", "1", "False"), + ("True", "1", "False"), + ("False", "2", "True"), + ("True", "1", "False"), + ("False", "2", "True"), + ("True", "1", "False"), + ("False", "2", "True"), + ("False", "2", "True"), + ), + 1, + ): + assert events[index].data == { + "first": result[0], + "index": result[1], + "last": result[2], + } + + @pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")]) async def test_choose(hass, var, result): """Test choose action.""" From 979dafd0a7451fc3260275fa8a8d169ae3cd3291 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jul 2020 16:03:42 -1000 Subject: [PATCH 10/51] Log which task is blocking startup when debug logging is on (#38134) * Log which task is blocking startup when debug logging for homeassistant.core is on * test needs to go one level deeper now --- homeassistant/core.py | 17 ++++++++++++++++- tests/helpers/test_entity_platform.py | 4 ++-- tests/test_core.py | 21 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index a8613dade59..01bfa402348 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -23,6 +23,7 @@ from typing import ( Callable, Coroutine, Dict, + Iterable, List, Mapping, Optional, @@ -98,6 +99,9 @@ CORE_STORAGE_VERSION = 1 DOMAIN = "homeassistant" +# How long to wait to log tasks that are blocking +BLOCK_LOG_TIMEOUT = 60 + # How long we wait for the result of a service call SERVICE_CALL_LIMIT = 10 # seconds @@ -393,10 +397,21 @@ class HomeAssistant: pending = [task for task in self._pending_tasks if not task.done()] self._pending_tasks.clear() if pending: - await asyncio.wait(pending) + await self._await_and_log_pending(pending) else: await asyncio.sleep(0) + async def _await_and_log_pending(self, pending: Iterable[Awaitable[Any]]) -> None: + """Await and log tasks that take a long time.""" + wait_time = 0 + while pending: + _, pending = await asyncio.wait(pending, timeout=BLOCK_LOG_TIMEOUT) + if not pending: + return + wait_time += BLOCK_LOG_TIMEOUT + for task in pending: + _LOGGER.debug("Waited %s seconds for task: %s", wait_time, task) + def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" if self.state == CoreState.not_running: # just ignore diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d5b817e0655..ddecd1988ed 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -188,8 +188,8 @@ async def test_platform_warn_slow_setup(hass): assert mock_call.called # mock_calls[0] is the warning message for component setup - # mock_calls[5] is the warning message for platform setup - timeout, logger_method = mock_call.mock_calls[5][1][:2] + # mock_calls[6] is the warning message for platform setup + timeout, logger_method = mock_call.mock_calls[6][1][:2] assert timeout == entity_platform.SLOW_SETUP_WARNING assert logger_method == _LOGGER.warning diff --git a/tests/test_core.py b/tests/test_core.py index 884c5e98125..3c0a9ee4fb8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1393,3 +1393,24 @@ async def test_start_events(hass): EVENT_HOMEASSISTANT_STARTED, ] assert core_states == [ha.CoreState.starting, ha.CoreState.running] + + +async def test_log_blocking_events(hass, caplog): + """Ensure we log which task is blocking startup when debug logging is on.""" + caplog.set_level(logging.DEBUG) + + async def _wait_a_bit_1(): + await asyncio.sleep(0.1) + + async def _wait_a_bit_2(): + await asyncio.sleep(0.1) + + hass.async_create_task(_wait_a_bit_1()) + await hass.async_block_till_done() + + with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.00001): + hass.async_create_task(_wait_a_bit_2()) + await hass.async_block_till_done() + + assert "_wait_a_bit_2" in caplog.text + assert "_wait_a_bit_1" not in caplog.text From 650f2babf951a4abcd4ccd7e833bf2228bb7500b Mon Sep 17 00:00:00 2001 From: Heiko Rothe Date: Fri, 24 Jul 2020 21:45:59 +0200 Subject: [PATCH 11/51] Fix Xbox Live integration (#38146) * Fix Xbox Live component The API moved to a different domain, so the integration was broken. The library upgrade contains the required fixes. * Fix API connectivity check * Access dict values directly --- .../components/xbox_live/manifest.json | 2 +- homeassistant/components/xbox_live/sensor.py | 38 +++++++++---------- requirements_all.txt | 2 +- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index f00f49c1589..3ebffc425ad 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -2,6 +2,6 @@ "domain": "xbox_live", "name": "Xbox Live", "documentation": "https://www.home-assistant.io/integrations/xbox_live", - "requirements": ["xboxapi==0.1.1"], + "requirements": ["xboxapi==2.0.0"], "codeowners": ["@MartinHjelmare"] } diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index ed5abe74bb6..1f46267967a 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import voluptuous as vol -from xboxapi import xbox_api +from xboxapi import Client from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL @@ -28,17 +28,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xbox platform.""" - api = xbox_api.XboxApi(config[CONF_API_KEY]) + api = Client(api_key=config[CONF_API_KEY]) entities = [] - # request personal profile to check api connection - profile = api.get_profile() - if profile.get("error_code") is not None: + # request profile info to check api connection + response = api.api_get("profile") + if not response.ok: _LOGGER.error( - "Can't setup XboxAPI connection. Check your account or " - "api key on xboxapi.com. Code: %s Description: %s ", - profile.get("error_code", "unknown"), - profile.get("error_message", "unknown"), + "Can't setup X API connection. Check your account or " + "api key on xapi.us. Code: %s Description: %s ", + response.status_code, + response.reason, ) return @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_user_gamercard(api, xuid): """Get profile info.""" - gamercard = api.get_user_gamercard(xuid) + gamercard = api.gamer(gamertag="", xuid=xuid).get("gamercard") _LOGGER.debug("User gamercard: %s", gamercard) if gamercard.get("success", True) and gamercard.get("code") is None: @@ -82,11 +82,11 @@ class XboxSensor(Entity): self._presence = [] self._xuid = xuid self._api = api - self._gamertag = gamercard.get("gamertag") - self._gamerscore = gamercard.get("gamerscore") + self._gamertag = gamercard["gamertag"] + self._gamerscore = gamercard["gamerscore"] self._interval = interval - self._picture = gamercard.get("gamerpicSmallSslImagePath") - self._tier = gamercard.get("tier") + self._picture = gamercard["gamerpicSmallSslImagePath"] + self._tier = gamercard["tier"] @property def name(self): @@ -111,10 +111,8 @@ class XboxSensor(Entity): attributes["tier"] = self._tier for device in self._presence: - for title in device.get("titles"): - attributes[ - f'{device.get("type")} {title.get("placement")}' - ] = title.get("name") + for title in device["titles"]: + attributes[f'{device["type"]} {title["placement"]}'] = title["name"] return attributes @@ -140,7 +138,7 @@ class XboxSensor(Entity): def update(self): """Update state data from Xbox API.""" - presence = self._api.get_user_presence(self._xuid) + presence = self._api.gamer(gamertag="", xuid=self._xuid).get("presence") _LOGGER.debug("User presence: %s", presence) - self._state = presence.get("state") + self._state = presence["state"] self._presence = presence.get("devices", []) diff --git a/requirements_all.txt b/requirements_all.txt index 3ff7a8ff322..68976257a46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2204,7 +2204,7 @@ wled==0.4.3 xbee-helper==0.0.7 # homeassistant.components.xbox_live -xboxapi==0.1.1 +xboxapi==2.0.0 # homeassistant.components.xfinity xfinity-gateway==0.0.4 From 953963c95ba48e7d0a8ab95c0debee8879d49111 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jul 2020 11:00:17 +0200 Subject: [PATCH 12/51] Fix incorrect mesurement in Toon for meter low (#38149) --- 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 d7f403f7013..5015d50fa63 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -224,7 +224,7 @@ SENSOR_ENTITIES = { "power_meter_reading_low": { ATTR_NAME: "Electricity Meter Feed IN Tariff 2", ATTR_SECTION: "power_usage", - ATTR_MEASUREMENT: "meter_high", + ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", From ad74d42b15c20f814d72b0ce1930fdb32623311b Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Fri, 24 Jul 2020 16:42:42 +0200 Subject: [PATCH 13/51] Fix Nuki Locks and Openers not being available after some time (#38159) --- CODEOWNERS | 2 +- homeassistant/components/nuki/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2d76eec1511..59c46c916ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -278,7 +278,7 @@ homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco -homeassistant/components/nuki/* @pvizeli +homeassistant/components/nuki/* @pschmitt @pvizeli homeassistant/components/numato/* @clssn homeassistant/components/nut/* @bdraco homeassistant/components/nws/* @MatthewFlamm diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 386b36a3ca9..09cf112d41c 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,6 +2,6 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.3.7"], - "codeowners": ["@pvizeli"] + "requirements": ["pynuki==1.3.8"], + "codeowners": ["@pschmitt", "@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index 68976257a46..3bcfc2e7596 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1488,7 +1488,7 @@ pynetgear==0.6.1 pynetio==0.1.9.1 # homeassistant.components.nuki -pynuki==1.3.7 +pynuki==1.3.8 # homeassistant.components.nut pynut2==2.1.2 From bbf36d1a36af522028b7821a91aacbf8c13bc08f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 24 Jul 2020 08:11:02 -0600 Subject: [PATCH 14/51] Remove leftover print statement (#38163) --- tests/components/simplisafe/test_config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index d5e22a48d8a..d94c431aa14 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -47,7 +47,6 @@ async def test_invalid_credentials(hass): """Test that invalid credentials throws an error.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - print("AARON") with patch( "simplipy.API.login_via_credentials", side_effect=InvalidCredentialsError, ): From 46dd245560e45f31262ee52168770255a1cbcf1b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jul 2020 21:57:56 +0200 Subject: [PATCH 15/51] Bumped version to 0.113.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1a4c1941e63..ddbc6ad91ed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 7f2a2ed23b855b39c88c0cccf4c51d387deec665 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jul 2020 21:00:08 -1000 Subject: [PATCH 16/51] Bump netdisco to 2.8.1 (#38173) * Bump netdisco to 2.8.1 * bump ssdp --- homeassistant/components/discovery/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 962ba9b8e8c..232237484d1 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.8.0"], + "requirements": ["netdisco==2.8.1"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3dde2e9002e..85ae4725e93 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["defusedxml==0.6.0", "netdisco==2.8.0"], + "requirements": ["defusedxml==0.6.0", "netdisco==2.8.1"], "after_dependencies": ["zeroconf"], "codeowners": [] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 31fdad0d54a..50e006c3d71 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ hass-nabucasa==0.34.7 home-assistant-frontend==20200716.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 -netdisco==2.8.0 +netdisco==2.8.1 paho-mqtt==1.5.0 pip>=8.0.3 python-slugify==4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3bcfc2e7596..2d0ed0cb07c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -936,7 +936,7 @@ netdata==0.2.0 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.0 +netdisco==2.8.1 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc48e7f7e49..f1365d9d598 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -431,7 +431,7 @@ nessclient==0.9.15 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.0 +netdisco==2.8.1 # homeassistant.components.nexia nexia==0.9.3 From 61fa5720680e73bdd90f071098288b585d2288d6 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sat, 25 Jul 2020 05:19:55 -0500 Subject: [PATCH 17/51] Stop automation runs when turned off or reloaded (#38174) * Add automation turn off / reload test * Stop automation runs when turned off or reloaded --- .../components/automation/__init__.py | 2 + tests/components/automation/test_init.py | 57 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3cbb98d85bd..2a05b1555c9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -455,6 +455,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._async_detach_triggers() self._async_detach_triggers = None + await self.action_script.async_stop() + self.async_write_ha_state() async def _async_attach_triggers( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 909493ddf53..bdae9a1f326 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,4 +1,6 @@ """The tests for the automation component.""" +import asyncio + import pytest from homeassistant.components import logbook @@ -12,10 +14,11 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, EVENT_HOMEASSISTANT_STARTED, + SERVICE_TURN_OFF, STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, CoreState, State +from homeassistant.core import Context, CoreState, State, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -553,6 +556,58 @@ async def test_reload_config_handles_load_fails(hass, calls): assert len(calls) == 2 +@pytest.mark.parametrize("service", ["turn_off", "reload"]) +async def test_automation_stops(hass, calls, service): + """Test that turning off / reloading an automation stops any running actions.""" + entity_id = "automation.hello" + test_entity = "test.entity" + + config = { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [ + {"event": "running"}, + {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, + {"service": "test.automation"}, + ], + } + } + assert await async_setup_component(hass, automation.DOMAIN, config,) + + running = asyncio.Event() + + @callback + def running_cb(event): + running.set() + + hass.bus.async_listen_once("running", running_cb) + hass.states.async_set(test_entity, "hello") + + hass.bus.async_fire("test_event") + await running.wait() + + if service == "turn_off": + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + else: + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ): + await common.async_reload(hass) + + hass.states.async_set(test_entity, "goodbye") + await hass.async_block_till_done() + + assert len(calls) == 0 + + async def test_automation_restore_state(hass): """Ensure states are restored on startup.""" time = dt_util.utcnow() From b82e64d9cbc3bcd71a7c34ef60e274a7c87b8134 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jul 2020 05:26:06 -1000 Subject: [PATCH 18/51] Bump tesla-powerwall to 0.2.12 to handle powerwall firmware 1.48+ (#38180) --- homeassistant/components/powerwall/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 1ba9562c4b7..930da38edde 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,6 +3,6 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.2.11"], + "requirements": ["tesla-powerwall==0.2.12"], "codeowners": ["@bdraco", "@jrester"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2d0ed0cb07c..672b1360af8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2080,7 +2080,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.powerwall -tesla-powerwall==0.2.11 +tesla-powerwall==0.2.12 # homeassistant.components.tesla teslajsonpy==0.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1365d9d598..73a0ccf2060 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -909,7 +909,7 @@ sunwatcher==0.2.1 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.2.11 +tesla-powerwall==0.2.12 # homeassistant.components.tesla teslajsonpy==0.9.3 From 2e89ec24f7cb9f63dfac5c4e4e29c4d958f4c1c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jul 2020 23:13:47 -1000 Subject: [PATCH 19/51] Ignore harmony hubs ips that are already configured during ssdp discovery (#38181) We would connect to the hub via discovery and via setup around the same time. This put additional load on the hub which can increase the risk of timeouts. --- .../components/harmony/config_flow.py | 10 ++++++++ tests/components/harmony/test_config_flow.py | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 576451ef2d6..9142eed2dba 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -86,6 +86,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] + if self._host_already_configured(parsed_url.hostname): + return self.async_abort(reason="already_configured") + # pylint: disable=no-member self.context["title_placeholders"] = {"name": friendly_name} @@ -158,6 +161,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=validated[CONF_NAME], data=data) + def _host_already_configured(self, host): + """See if we already have a harmony entry matching the host.""" + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == host: + return True + return False + def _options_from_user_input(user_input): options = {} diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 0228983ef9d..d159a0f025f 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -145,6 +145,30 @@ async def test_form_ssdp(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): + """Test we abort without connecting if the host is already known.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, data={"host": "2.2.2.2", "name": "any"}, + ) + config_entry.add_to_hass(hass) + + harmonyapi = _get_mock_harmonyapi(connect=True) + + with patch( + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "Harmony Hub", + "ssdp_location": "http://2.2.2.2:8088/description", + }, + ) + assert result["type"] == "abort" + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( From ee0c32cbb7e64c8210627952a7e14d86e47e2b00 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 25 Jul 2020 22:56:58 +0200 Subject: [PATCH 20/51] Make rfxtrx RfyDevices have sun automation switches (#38210) * RfyDevices have sun automation * We must accept sun automation commands for switch * Add test for Rfy sun automation --- homeassistant/components/rfxtrx/const.py | 2 ++ homeassistant/components/rfxtrx/switch.py | 1 + tests/components/rfxtrx/test_switch.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 7626c082f45..500fb3a1d1b 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -7,12 +7,14 @@ COMMAND_ON_LIST = [ "Stop", "Open (inline relay)", "Stop (inline relay)", + "Enable sun automation", ] COMMAND_OFF_LIST = [ "Off", "Down", "Close (inline relay)", + "Disable sun automation", ] ATTR_EVENT = "event" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 6cd9a484abd..52a55f79c01 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -37,6 +37,7 @@ async def async_setup_entry( isinstance(event.device, rfxtrxmod.LightingDevice) and not event.device.known_to_be_dimmable and not event.device.known_to_be_rollershutter + or isinstance(event.device, rfxtrxmod.RfyDevice) ) # Add switch from config file diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 22f7a73c77c..1f43081a9b5 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -10,6 +10,9 @@ from . import _signal_event from tests.common import mock_restore_cache +EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" +EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" + async def test_one_switch(hass, rfxtrx): """Test with 1 switch.""" @@ -139,3 +142,18 @@ async def test_discover_switch(hass, rfxtrx): state = hass.states.get("switch.ac_118cdeb_2") assert state assert state.state == "on" + + +async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic): + """Test with discovery of switches.""" + rfxtrx = rfxtrx_automatic + + await rfxtrx.signal(EVENT_RFY_DISABLE_SUN_AUTO) + state = hass.states.get("switch.rfy_030101_1") + assert state + assert state.state == "off" + + await rfxtrx.signal(EVENT_RFY_ENABLE_SUN_AUTO) + state = hass.states.get("switch.rfy_030101_1") + assert state + assert state.state == "on" From e89c475856b8fb000c882017f5b9308560381213 Mon Sep 17 00:00:00 2001 From: Jeroen Van den Keybus Date: Tue, 28 Jul 2020 01:40:21 +0200 Subject: [PATCH 21/51] Fix detection of zones 2 and 3 in Onkyo/Pioneer amplifiers (#38234) --- homeassistant/components/onkyo/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 30f4ae0800a..da33ff5f018 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -120,7 +120,7 @@ def determine_zones(receiver): out = {"zone2": False, "zone3": False} try: _LOGGER.debug("Checking for zone 2 capability") - receiver.raw("ZPW") + receiver.raw("ZPWQSTN") out["zone2"] = True except ValueError as error: if str(error) != TIMEOUT_MESSAGE: @@ -128,7 +128,7 @@ def determine_zones(receiver): _LOGGER.debug("Zone 2 timed out, assuming no functionality") try: _LOGGER.debug("Checking for zone 3 capability") - receiver.raw("PW3") + receiver.raw("PW3QSTN") out["zone3"] = True except ValueError as error: if str(error) != TIMEOUT_MESSAGE: From bdab4375742f0f0db585f88c2e1341f3bff9ad23 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Mon, 27 Jul 2020 16:51:34 -0500 Subject: [PATCH 22/51] Fix repeat action when variables present (#38237) --- homeassistant/helpers/script.py | 38 +++++++++++++++++---- tests/helpers/test_script.py | 58 +++++++++++++++++++++------------ 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1eb46a1eded..b485ebdea4e 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,7 +4,19 @@ from datetime import datetime from functools import partial import itertools import logging -from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple +from types import MappingProxyType +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Set, + Tuple, + Union, + cast, +) from async_timeout import timeout import voluptuous as vol @@ -134,13 +146,13 @@ class _ScriptRun: self, hass: HomeAssistant, script: "Script", - variables: Optional[Sequence], + variables: Dict[str, Any], context: Optional[Context], log_exceptions: bool, ) -> None: self._hass = hass self._script = script - self._variables = variables or {} + self._variables = variables self._context = context self._log_exceptions = log_exceptions self._step = -1 @@ -595,6 +607,9 @@ async def _async_stop_scripts_at_shutdown(hass, event): ) +_VarsType = Union[Dict[str, Any], MappingProxyType] + + class Script: """Representation of a script.""" @@ -617,6 +632,7 @@ class Script: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) ) + self._top_level = top_level if top_level: all_scripts.append( {"instance": self, "started_before_shutdown": not hass.is_stopping} @@ -732,14 +748,16 @@ class Script: self._referenced_entities = referenced return referenced - def run(self, variables=None, context=None): + def run( + self, variables: Optional[_VarsType] = None, context: Optional[Context] = None + ) -> None: """Run script.""" asyncio.run_coroutine_threadsafe( self.async_run(variables, context), self._hass.loop ).result() async def async_run( - self, variables: Optional[Sequence] = None, context: Optional[Context] = None + self, variables: Optional[_VarsType] = None, context: Optional[Context] = None ) -> None: """Run script.""" if self.is_running: @@ -753,11 +771,19 @@ class Script: self._log("Maximum number of runs exceeded", level=logging.WARNING) return + # If this is a top level Script then make a copy of the variables in case they + # are read-only, but more importantly, so as not to leak any variables created + # during the run back to the caller. + if self._top_level: + variables = dict(variables) if variables is not None else {} + if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun else: cls = _QueuedScriptRun - run = cls(self._hass, self, variables, context, self._log_exceptions) + run = cls( + self._hass, self, cast(dict, variables), context, self._log_exceptions + ) self._runs.append(run) try: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 7a458c49286..29211e03119 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4,6 +4,7 @@ import asyncio from contextlib import contextmanager from datetime import timedelta import logging +from types import MappingProxyType from unittest import mock import pytest @@ -122,7 +123,7 @@ async def test_firing_event_template(hass): ) script_obj = script.Script(hass, sequence) - await script_obj.async_run({"is_world": "yes"}, context=context) + await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context) await hass.async_block_till_done() assert len(events) == 1 @@ -175,7 +176,7 @@ async def test_calling_service_template(hass): ) script_obj = script.Script(hass, sequence) - await script_obj.async_run({"is_world": "yes"}, context=context) + await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context) await hass.async_block_till_done() assert len(calls) == 1 @@ -235,7 +236,9 @@ async def test_multiple_runs_no_wait(hass): logger.debug("starting 1st script") hass.async_create_task( script_obj.async_run( - {"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"} + MappingProxyType( + {"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"} + ) ) ) await asyncio.wait_for(heard_event.wait(), 1) @@ -243,7 +246,7 @@ async def test_multiple_runs_no_wait(hass): logger.debug("starting 2nd script") await script_obj.async_run( - {"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"} + MappingProxyType({"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"}) ) await hass.async_block_till_done() @@ -670,7 +673,9 @@ async def test_wait_template_variables(hass): try: hass.states.async_set("switch.test", "on") - hass.async_create_task(script_obj.async_run({"data": "switch.test"})) + hass.async_create_task( + script_obj.async_run(MappingProxyType({"data": "switch.test"})) + ) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running @@ -882,7 +887,14 @@ async def test_repeat_var_in_condition(hass, condition): assert len(events) == 2 -async def test_repeat_nested(hass): +@pytest.mark.parametrize( + "variables,first_last,inside_x", + [ + (None, {"repeat": "None", "x": "None"}, "None"), + (MappingProxyType({"x": 1}), {"repeat": "None", "x": "1"}, "1"), + ], +) +async def test_repeat_nested(hass, variables, first_last, inside_x): """Test nested repeats.""" event = "test_event" events = async_capture_events(hass, event) @@ -892,7 +904,8 @@ async def test_repeat_nested(hass): { "event": event, "event_data_template": { - "repeat": "{{ None if repeat is not defined else repeat }}" + "repeat": "{{ None if repeat is not defined else repeat }}", + "x": "{{ None if x is not defined else x }}", }, }, { @@ -905,6 +918,7 @@ async def test_repeat_nested(hass): "first": "{{ repeat.first }}", "index": "{{ repeat.index }}", "last": "{{ repeat.last }}", + "x": "{{ None if x is not defined else x }}", }, }, { @@ -916,6 +930,7 @@ async def test_repeat_nested(hass): "first": "{{ repeat.first }}", "index": "{{ repeat.index }}", "last": "{{ repeat.last }}", + "x": "{{ None if x is not defined else x }}", }, }, } @@ -926,6 +941,7 @@ async def test_repeat_nested(hass): "first": "{{ repeat.first }}", "index": "{{ repeat.index }}", "last": "{{ repeat.last }}", + "x": "{{ None if x is not defined else x }}", }, }, ], @@ -934,7 +950,8 @@ async def test_repeat_nested(hass): { "event": event, "event_data_template": { - "repeat": "{{ None if repeat is not defined else repeat }}" + "repeat": "{{ None if repeat is not defined else repeat }}", + "x": "{{ None if x is not defined else x }}", }, }, ] @@ -945,21 +962,21 @@ async def test_repeat_nested(hass): "homeassistant.helpers.condition._LOGGER.error", side_effect=AssertionError("Template Error"), ): - await script_obj.async_run() + await script_obj.async_run(variables) assert len(events) == 10 - assert events[0].data == {"repeat": "None"} - assert events[-1].data == {"repeat": "None"} + assert events[0].data == first_last + assert events[-1].data == first_last for index, result in enumerate( ( - ("True", "1", "False"), - ("True", "1", "False"), - ("False", "2", "True"), - ("True", "1", "False"), - ("False", "2", "True"), - ("True", "1", "False"), - ("False", "2", "True"), - ("False", "2", "True"), + ("True", "1", "False", inside_x), + ("True", "1", "False", inside_x), + ("False", "2", "True", inside_x), + ("True", "1", "False", inside_x), + ("False", "2", "True", inside_x), + ("True", "1", "False", inside_x), + ("False", "2", "True", inside_x), + ("False", "2", "True", inside_x), ), 1, ): @@ -967,6 +984,7 @@ async def test_repeat_nested(hass): "first": result[0], "index": result[1], "last": result[2], + "x": result[3], } @@ -998,7 +1016,7 @@ async def test_choose(hass, var, result): ) script_obj = script.Script(hass, sequence) - await script_obj.async_run({"var": var}) + await script_obj.async_run(MappingProxyType({"var": var})) await hass.async_block_till_done() assert len(events) == 1 From 293db61b329c22eb090c1ded31f37b080ce6ffb8 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Mon, 27 Jul 2020 03:43:58 -0500 Subject: [PATCH 23/51] Fix parallel script containing repeat or choose action with max_runs > 10 (#38243) --- homeassistant/helpers/script.py | 3 +++ tests/helpers/test_script.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index b485ebdea4e..6929d8a3bd3 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -821,6 +821,7 @@ class Script: action[CONF_REPEAT][CONF_SEQUENCE], f"{self.name}: {step_name}", script_mode=SCRIPT_MODE_PARALLEL, + max_runs=self.max_runs, logger=self._logger, top_level=False, ) @@ -848,6 +849,7 @@ class Script: choice[CONF_SEQUENCE], f"{self.name}: {step_name}: choice {idx}", script_mode=SCRIPT_MODE_PARALLEL, + max_runs=self.max_runs, logger=self._logger, top_level=False, ) @@ -862,6 +864,7 @@ class Script: action[CONF_DEFAULT], f"{self.name}: {step_name}: default", script_mode=SCRIPT_MODE_PARALLEL, + max_runs=self.max_runs, logger=self._logger, top_level=False, ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 29211e03119..33dec87ccd8 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1023,6 +1023,31 @@ async def test_choose(hass, var, result): assert events[0].data["choice"] == result +@pytest.mark.parametrize( + "action", + [ + {"repeat": {"count": 1, "sequence": {"event": "abc"}}}, + {"choose": {"conditions": [], "sequence": {"event": "abc"}}}, + {"choose": [], "default": {"event": "abc"}}, + ], +) +async def test_multiple_runs_repeat_choose(hass, caplog, action): + """Test parallel runs with repeat & choose actions & max_runs > default.""" + max_runs = script.DEFAULT_MAX + 1 + script_obj = script.Script( + hass, cv.SCRIPT_SCHEMA(action), script_mode="parallel", max_runs=max_runs + ) + + events = async_capture_events(hass, "abc") + for _ in range(max_runs): + hass.async_create_task(script_obj.async_run()) + await hass.async_block_till_done() + + assert "WARNING" not in caplog.text + assert "ERROR" not in caplog.text + assert len(events) == max_runs + + async def test_last_triggered(hass): """Test the last_triggered.""" event = "test_event" From c55c4159336f8ac3a65a5dec3020f452e957f6f4 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Sun, 26 Jul 2020 23:08:01 -0700 Subject: [PATCH 24/51] Fix Skybell useragent (#38245) --- homeassistant/components/skybell/__init__.py | 15 +++++++++++++-- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index a4e4263d360..9606d9bcf12 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -5,7 +5,12 @@ from requests.exceptions import ConnectTimeout, HTTPError from skybellpy import Skybell import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_PASSWORD, + CONF_USERNAME, + __version__, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -20,6 +25,8 @@ DOMAIN = "skybell" DEFAULT_CACHEDB = "./skybell_cache.pickle" DEFAULT_ENTITY_NAMESPACE = "skybell" +AGENT_IDENTIFIER = f"HomeAssistant/{__version__}" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -42,7 +49,11 @@ def setup(hass, config): try: cache = hass.config.path(DEFAULT_CACHEDB) skybell = Skybell( - username=username, password=password, get_devices=True, cache_path=cache + username=username, + password=password, + get_devices=True, + cache_path=cache, + agent_identifier=AGENT_IDENTIFIER, ) hass.data[DOMAIN] = skybell diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 9e0a0be8905..1b97b800956 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -2,6 +2,6 @@ "domain": "skybell", "name": "SkyBell", "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["skybellpy==0.4.0"], + "requirements": ["skybellpy==0.6.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 672b1360af8..f123e4f0f4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1945,7 +1945,7 @@ simplisafe-python==9.2.1 sisyphus-control==2.2.1 # homeassistant.components.skybell -skybellpy==0.4.0 +skybellpy==0.6.1 # homeassistant.components.slack slackclient==2.5.0 From 37e029b2c1820d27d23c98d07a28fc426c66fcfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Jul 2020 17:01:29 -1000 Subject: [PATCH 25/51] Improve setup retry logic to handle inconsistent powerview hub availability (#38249) --- .../hunterdouglas_powerview/__init__.py | 26 ++++++++++++------- .../hunterdouglas_powerview/const.py | 3 ++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 3df895c94ce..3f78726bf30 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -112,22 +112,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: async with async_timeout.timeout(10): device_info = await async_get_device_info(pv_request) + + async with async_timeout.timeout(10): + rooms = Rooms(pv_request) + room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) + + async with async_timeout.timeout(10): + scenes = Scenes(pv_request) + scene_data = _async_map_data_by_id( + (await scenes.get_resources())[SCENE_DATA] + ) + + async with async_timeout.timeout(10): + shades = Shades(pv_request) + shade_data = _async_map_data_by_id( + (await shades.get_resources())[SHADE_DATA] + ) except HUB_EXCEPTIONS: _LOGGER.error("Connection error to PowerView hub: %s", hub_address) raise ConfigEntryNotReady + if not device_info: _LOGGER.error("Unable to initialize PowerView hub: %s", hub_address) raise ConfigEntryNotReady - rooms = Rooms(pv_request) - room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) - - scenes = Scenes(pv_request) - scene_data = _async_map_data_by_id((await scenes.get_resources())[SCENE_DATA]) - - shades = Shades(pv_request) - shade_data = _async_map_data_by_id((await shades.get_resources())[SHADE_DATA]) - async def async_update_data(): """Fetch data from shade endpoint.""" async with async_timeout.timeout(10): diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index e69fe319c0f..e83a9d8945b 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -2,6 +2,7 @@ import asyncio +from aiohttp.client_exceptions import ServerDisconnectedError from aiopvapi.helpers.aiorequest import PvApiConnectionError DOMAIN = "hunterdouglas_powerview" @@ -64,7 +65,7 @@ PV_SHADE_DATA = "pv_shade_data" PV_ROOM_DATA = "pv_room_data" COORDINATOR = "coordinator" -HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError) +HUB_EXCEPTIONS = (ServerDisconnectedError, asyncio.TimeoutError, PvApiConnectionError) LEGACY_DEVICE_SUB_REVISION = 1 LEGACY_DEVICE_REVISION = 0 From 8768fe1652947240bef192a3e84869e2293e63c2 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 27 Jul 2020 16:15:28 +0100 Subject: [PATCH 26/51] Don't set up callbacks until entity is created. (#38251) --- homeassistant/components/vera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index b636477b16d..263f5f0025b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -194,7 +194,9 @@ class VeraDevice(Entity): slugify(vera_device.name), vera_device.device_id ) - self.controller.register(vera_device, self._update_callback) + async def async_added_to_hass(self): + """Subscribe to updates.""" + self.controller.register(self.vera_device, self._update_callback) def _update_callback(self, _device): """Update the state.""" From bd1336cbdf475727716cb6463dd521fd30b7a392 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Jul 2020 20:51:53 -1000 Subject: [PATCH 27/51] Prevent onvif from blocking startup (#38256) --- homeassistant/components/onvif/event.py | 43 +++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 9084a06e7db..60a92e56ac0 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -6,7 +6,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError from onvif import ONVIFCamera, ONVIFService from zeep.exceptions import Fault -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -114,30 +114,31 @@ class EventManager: async def async_pull_messages(self, _now: dt = None) -> None: """Pull messages from device.""" - try: - pullpoint = self.device.create_pullpoint_service() - req = pullpoint.create_type("PullMessages") - req.MessageLimit = 100 - req.Timeout = dt.timedelta(seconds=60) - response = await pullpoint.PullMessages(req) + if self.hass.state == CoreState.running: + try: + pullpoint = self.device.create_pullpoint_service() + req = pullpoint.create_type("PullMessages") + req.MessageLimit = 100 + req.Timeout = dt.timedelta(seconds=60) + response = await pullpoint.PullMessages(req) - # Renew subscription if less than two hours is left - if ( - dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() - ).total_seconds() < 7200: - await self.async_renew() + # Renew subscription if less than two hours is left + if ( + dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() + ).total_seconds() < 7200: + await self.async_renew() - # Parse response - await self.async_parse_messages(response.NotificationMessage) + # Parse response + await self.async_parse_messages(response.NotificationMessage) - except ServerDisconnectedError: - pass - except Fault: - pass + except ServerDisconnectedError: + pass + except Fault: + pass - # Update entities - for update_callback in self._listeners: - update_callback() + # Update entities + for update_callback in self._listeners: + update_callback() # Reschedule another pull if self._listeners: From b5a64b3752e165004ec004e1d329c23733ff8504 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Mon, 27 Jul 2020 15:56:39 -0300 Subject: [PATCH 28/51] Fix #38289 issue with xboxapi lib (#38293) --- homeassistant/components/xbox_live/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 3ebffc425ad..937f33bd009 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -2,6 +2,6 @@ "domain": "xbox_live", "name": "Xbox Live", "documentation": "https://www.home-assistant.io/integrations/xbox_live", - "requirements": ["xboxapi==2.0.0"], + "requirements": ["xboxapi==2.0.1"], "codeowners": ["@MartinHjelmare"] } diff --git a/requirements_all.txt b/requirements_all.txt index f123e4f0f4d..a054e273181 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2204,7 +2204,7 @@ wled==0.4.3 xbee-helper==0.0.7 # homeassistant.components.xbox_live -xboxapi==2.0.0 +xboxapi==2.0.1 # homeassistant.components.xfinity xfinity-gateway==0.0.4 From c4038c8652eb1ad2ebfde6133bfef096c9f43b90 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 28 Jul 2020 04:26:29 +0200 Subject: [PATCH 29/51] Bump python-miio to 0.5.3 (#38300) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 471dc7290df..09719d720c0 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.9.45", "python-miio==0.5.2.1"], + "requirements": ["construct==2.9.45", "python-miio==0.5.3"], "codeowners": ["@rytilahti", "@syssi"], "zeroconf": ["_miio._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a054e273181..ea30023f8b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1701,7 +1701,7 @@ python-juicenet==1.0.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.2.1 +python-miio==0.5.3 # homeassistant.components.mpd python-mpd2==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73a0ccf2060..0efa7c3664f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -767,7 +767,7 @@ python-izone==1.1.2 python-juicenet==1.0.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.2.1 +python-miio==0.5.3 # homeassistant.components.nest python-nest==4.1.0 From b8d6b20c96f5421acd5a0542890af27c5237c943 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jul 2020 19:57:36 -1000 Subject: [PATCH 30/51] Prevent speedtest from blocking startup or causing other intergations to fail setup (#38305) When speedtest starts up, it would saturate the network interface and cause other integrations to randomly fail to setup. We now wait to do the first speed test until after the started event is fired. --- .../components/speedtestdotnet/__init__.py | 34 +++++++++++++------ .../components/speedtestdotnet/sensor.py | 8 ++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2ef49877031..6fd2dec5efd 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -6,7 +6,12 @@ import speedtest import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STARTED, +) +from homeassistant.core import CoreState from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -70,10 +75,25 @@ async def async_setup_entry(hass, config_entry): coordinator = SpeedTestDataCoordinator(hass, config_entry) await coordinator.async_setup() - if not config_entry.options[CONF_MANUAL]: + async def _enable_scheduled_speedtests(*_): + """Activate the data update coordinator.""" + coordinator.update_interval = timedelta( + minutes=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ) await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + + if not config_entry.options[CONF_MANUAL]: + if hass.state == CoreState.running: + await _enable_scheduled_speedtests() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + else: + # Running a speed test during startup can prevent + # integrations from being able to setup because it + # can saturate the network interface. + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _enable_scheduled_speedtests + ) hass.data[DOMAIN] = coordinator @@ -107,12 +127,6 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): super().__init__( self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update, ) - if not self.config_entry.options.get(CONF_MANUAL): - self.update_interval = timedelta( - minutes=self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - ) def update_servers(self): """Update list of test servers.""" diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 0889d7da5b2..d071a226a05 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -12,7 +12,6 @@ from .const import ( ATTR_SERVER_ID, ATTR_SERVER_NAME, ATTRIBUTION, - CONF_MANUAL, DEFAULT_NAME, DOMAIN, ICON, @@ -97,10 +96,9 @@ class SpeedtestSensor(RestoreEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() - if self.coordinator.config_entry.options[CONF_MANUAL]: - state = await self.async_get_last_state() - if state: - self._state = state.state + state = await self.async_get_last_state() + if state: + self._state = state.state @callback def update(): From f8fbe8dba301456dcdf8ecc71fb8efa514829015 Mon Sep 17 00:00:00 2001 From: Kyle Hendricks Date: Tue, 28 Jul 2020 06:03:56 -0400 Subject: [PATCH 31/51] Fix issue with certain Samsung TVs repeatedly showing auth dialog (#38308) Through some testing with the samsungtvws library, it was determined that the issue is related to the short read timeout (1s). Increasing the timeout to 10s should solve the issue. --- homeassistant/components/samsungtv/bridge.py | 2 +- tests/components/samsungtv/test_media_player.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 472ce894e1a..83b8ea3d138 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -250,7 +250,7 @@ class SamsungTVWSBridge(SamsungTVBridge): host=self.host, port=self.port, token=self.token, - timeout=1, + timeout=10, name=VALUE_CONF_NAME, ) self._remote.open() diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 15ac13c64d5..5d6c36153c2 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = { "host": "fake", "name": "HomeAssistant", "port": 8001, - "timeout": 1, + "timeout": 10, "token": "abcde", } From e984e0d4148d621e364f1a86cb3cb505cb43fd60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jul 2020 06:24:29 -1000 Subject: [PATCH 32/51] Add debug logging for when a chain of tasks blocks startup (#38311) Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- homeassistant/core.py | 16 +++++++++++++ tests/test_core.py | 55 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 01bfa402348..a40bc18c804 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -392,12 +392,28 @@ class HomeAssistant: """Block until all pending work is done.""" # To flush out any call_soon_threadsafe await asyncio.sleep(0) + start_time: Optional[float] = None while self._pending_tasks: pending = [task for task in self._pending_tasks if not task.done()] self._pending_tasks.clear() if pending: await self._await_and_log_pending(pending) + + if start_time is None: + # Avoid calling monotonic() until we know + # we may need to start logging blocked tasks. + start_time = 0 + elif start_time == 0: + # If we have waited twice then we set the start + # time + start_time = monotonic() + elif monotonic() - start_time > BLOCK_LOG_TIMEOUT: + # We have waited at least three loops and new tasks + # continue to block. At this point we start + # logging all waiting tasks. + for task in pending: + _LOGGER.debug("Waiting for task: %s", task) else: await asyncio.sleep(0) diff --git a/tests/test_core.py b/tests/test_core.py index 3c0a9ee4fb8..a63f42af61b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1408,9 +1408,62 @@ async def test_log_blocking_events(hass, caplog): hass.async_create_task(_wait_a_bit_1()) await hass.async_block_till_done() - with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.00001): + with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001): hass.async_create_task(_wait_a_bit_2()) await hass.async_block_till_done() assert "_wait_a_bit_2" in caplog.text assert "_wait_a_bit_1" not in caplog.text + + +async def test_chained_logging_hits_log_timeout(hass, caplog): + """Ensure we log which task is blocking startup when there is a task chain and debug logging is on.""" + caplog.set_level(logging.DEBUG) + + created = 0 + + async def _task_chain_1(): + nonlocal created + created += 1 + if created > 10: + return + hass.async_create_task(_task_chain_2()) + + async def _task_chain_2(): + nonlocal created + created += 1 + if created > 10: + return + hass.async_create_task(_task_chain_1()) + + with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001): + hass.async_create_task(_task_chain_1()) + await hass.async_block_till_done() + + assert "_task_chain_" in caplog.text + + +async def test_chained_logging_misses_log_timeout(hass, caplog): + """Ensure we do not log which task is blocking startup if we do not hit the timeout.""" + caplog.set_level(logging.DEBUG) + + created = 0 + + async def _task_chain_1(): + nonlocal created + created += 1 + if created > 10: + return + hass.async_create_task(_task_chain_2()) + + async def _task_chain_2(): + nonlocal created + created += 1 + if created > 10: + return + hass.async_create_task(_task_chain_1()) + + hass.async_create_task(_task_chain_1()) + await hass.async_block_till_done() + + assert "_task_chain_" not in caplog.text From d9953a8c2f242d021d363dba69ce8ce62b70f45b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jul 2020 17:51:35 +0200 Subject: [PATCH 33/51] Remove AdGuard version check (#38326) --- homeassistant/components/adguard/__init__.py | 10 +--- .../components/adguard/config_flow.py | 25 ++------- homeassistant/components/adguard/const.py | 2 - homeassistant/components/adguard/strings.json | 2 - tests/components/adguard/test_config_flow.py | 51 +------------------ 5 files changed, 5 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 84e86bfcaba..71dff2ab6ee 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,5 +1,4 @@ """Support for AdGuard Home.""" -from distutils.version import LooseVersion import logging from typing import Any, Dict @@ -11,7 +10,6 @@ from homeassistant.components.adguard.const import ( DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, - MIN_ADGUARD_HOME_VERSION, SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, @@ -67,16 +65,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard try: - version = await adguard.version() + await adguard.version() except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): - _LOGGER.error( - "This integration requires AdGuard Home v0.99.0 or higher to work correctly" - ) - raise ConfigEntryNotReady - for component in "sensor", "switch": hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index ede01706c5d..a0ace623862 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,12 +1,11 @@ """Config flow to configure the AdGuard Home integration.""" -from distutils.version import LooseVersion import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION +from homeassistant.components.adguard.const import DOMAIN from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -79,20 +78,11 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - version = await adguard.version() + await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_setup_form(errors) - if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): - return self.async_abort( - reason="adguard_home_outdated", - description_placeholders={ - "current_version": version, - "minimal_version": MIN_ADGUARD_HOME_VERSION, - }, - ) - return self.async_create_entry( title=user_input[CONF_HOST], data={ @@ -160,20 +150,11 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - version = await adguard.version() + await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_hassio_form(errors) - if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): - return self.async_abort( - reason="adguard_home_addon_outdated", - description_placeholders={ - "current_version": version, - "minimal_version": MIN_ADGUARD_HOME_VERSION, - }, - ) - return self.async_create_entry( title=self._hassio_discovery["addon"], data={ diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index eb12a9c163f..c77d76a70cf 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -7,8 +7,6 @@ DATA_ADGUARD_VERION = "adguard_version" CONF_FORCE = "force" -MIN_ADGUARD_HOME_VERSION = "v0.99.0" - SERVICE_ADD_URL = "add_url" SERVICE_DISABLE_URL = "disable_url" SERVICE_ENABLE_URL = "enable_url" diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index f5f780c70b5..f010f9e2ade 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -19,8 +19,6 @@ }, "error": { "connection_error": "Failed to connect." }, "abort": { - "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", - "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." } diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index d0e874bacdc..69e1285889b 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -4,7 +4,7 @@ import aiohttp from homeassistant import config_entries, data_entry_flow from homeassistant.components.adguard import config_flow -from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION +from homeassistant.components.adguard.const import DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -229,52 +229,3 @@ async def test_hassio_connection_error(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "hassio_confirm" assert result["errors"] == {"base": "connection_error"} - - -async def test_outdated_adguard_version(hass, aioclient_mock): - """Test we show abort when connecting with unsupported AdGuard version.""" - aioclient_mock.get( - f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" - f"://{FIXTURE_USER_INPUT[CONF_HOST]}" - f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status", - json={"version": "v0.98.0"}, - headers={"Content-Type": "application/json"}, - ) - - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "adguard_home_outdated" - assert result["description_placeholders"] == { - "current_version": "v0.98.0", - "minimal_version": MIN_ADGUARD_HOME_VERSION, - } - - -async def test_outdated_adguard_addon_version(hass, aioclient_mock): - """Test we show abort when connecting with unsupported AdGuard add-on version.""" - aioclient_mock.get( - "http://mock-adguard:3000/control/status", - json={"version": "v0.98.0"}, - headers={"Content-Type": "application/json"}, - ) - - result = await hass.config_entries.flow.async_init( - "adguard", - data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, - context={"source": "hassio"}, - ) - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "adguard_home_addon_outdated" - assert result["description_placeholders"] == { - "current_version": "v0.98.0", - "minimal_version": MIN_ADGUARD_HOME_VERSION, - } From 612e27b5ffd9c59de31b3c4242262aab2730ba42 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jul 2020 19:37:50 +0200 Subject: [PATCH 34/51] Bumped version to 0.113.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ddbc6ad91ed..5c5877c5e7b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 60cab62da51f71a22847d62e1f1dc5a03b358d3a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jul 2020 20:05:31 +0200 Subject: [PATCH 35/51] Revert "Make rfxtrx RfyDevices have sun automation switches (#38210)" This reverts commit ee0c32cbb7e64c8210627952a7e14d86e47e2b00. --- homeassistant/components/rfxtrx/const.py | 2 -- homeassistant/components/rfxtrx/switch.py | 1 - tests/components/rfxtrx/test_switch.py | 18 ------------------ 3 files changed, 21 deletions(-) diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 500fb3a1d1b..7626c082f45 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -7,14 +7,12 @@ COMMAND_ON_LIST = [ "Stop", "Open (inline relay)", "Stop (inline relay)", - "Enable sun automation", ] COMMAND_OFF_LIST = [ "Off", "Down", "Close (inline relay)", - "Disable sun automation", ] ATTR_EVENT = "event" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 52a55f79c01..6cd9a484abd 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -37,7 +37,6 @@ async def async_setup_entry( isinstance(event.device, rfxtrxmod.LightingDevice) and not event.device.known_to_be_dimmable and not event.device.known_to_be_rollershutter - or isinstance(event.device, rfxtrxmod.RfyDevice) ) # Add switch from config file diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 1f43081a9b5..22f7a73c77c 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -10,9 +10,6 @@ from . import _signal_event from tests.common import mock_restore_cache -EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" -EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" - async def test_one_switch(hass, rfxtrx): """Test with 1 switch.""" @@ -142,18 +139,3 @@ async def test_discover_switch(hass, rfxtrx): state = hass.states.get("switch.ac_118cdeb_2") assert state assert state.state == "on" - - -async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic): - """Test with discovery of switches.""" - rfxtrx = rfxtrx_automatic - - await rfxtrx.signal(EVENT_RFY_DISABLE_SUN_AUTO) - state = hass.states.get("switch.rfy_030101_1") - assert state - assert state.state == "off" - - await rfxtrx.signal(EVENT_RFY_ENABLE_SUN_AUTO) - state = hass.states.get("switch.rfy_030101_1") - assert state - assert state.state == "on" From 7a2f6a500646ed8835b7a2740edd640689be0a61 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sun, 26 Jul 2020 11:59:11 -0700 Subject: [PATCH 36/51] Add Abode camera on and off support (#35164) * Add Abode camera controls * Add tests for camera turn on and off service * Bump abodepy version * Bump abodepy version and updates to reflect changes * Update manifest --- homeassistant/components/abode/__init__.py | 1 + homeassistant/components/abode/camera.py | 13 +++++++++ homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/abode/test_camera.py | 30 ++++++++++++++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 85e05e89cc1..92665bc1890 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -261,6 +261,7 @@ def setup_abode_events(hass): TIMELINE.AUTOMATION_GROUP, TIMELINE.DISARM_GROUP, TIMELINE.ARM_GROUP, + TIMELINE.ARM_FAULT_GROUP, TIMELINE.TEST_GROUP, TIMELINE.CAPTURE_GROUP, TIMELINE.DEVICE_GROUP, diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index b7d5f1dbe4c..99d4fd433a7 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -82,8 +82,21 @@ class AbodeCamera(AbodeDevice, Camera): return None + def turn_on(self): + """Turn on camera.""" + self._device.privacy_mode(False) + + def turn_off(self): + """Turn off camera.""" + self._device.privacy_mode(True) + def _capture_callback(self, capture): """Update the image with the device then refresh device.""" self._device.update_image_location(capture) self.get_image() self.schedule_update_ha_state() + + @property + def is_on(self): + """Return true if on.""" + return self._device.is_on diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index d59ddd6217f..e9a871035e6 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,7 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["abodepy==0.19.0"], + "requirements": ["abodepy==1.1.0"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] diff --git a/requirements_all.txt b/requirements_all.txt index ea30023f8b9..bf08943341d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ WazeRouteCalculator==0.12 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.19.0 +abodepy==1.1.0 # homeassistant.components.mcp23017 adafruit-blinka==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0efa7c3664f..6bdf6919659 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ WSDiscovery==2.0.0 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.19.0 +abodepy==1.1.0 # homeassistant.components.androidtv adb-shell[async]==0.2.0 diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index 0e843c59023..9db03d90222 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -38,3 +38,33 @@ async def test_capture_image(hass): ) await hass.async_block_till_done() mock_capture.assert_called_once() + + +async def test_camera_on(hass): + """Test the camera turn on service.""" + await setup_platform(hass, CAMERA_DOMAIN) + + with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + await hass.services.async_call( + CAMERA_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: "camera.test_cam"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_capture.assert_called_once_with(False) + + +async def test_camera_off(hass): + """Test the camera turn off service.""" + await setup_platform(hass, CAMERA_DOMAIN) + + with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + await hass.services.async_call( + CAMERA_DOMAIN, + "turn_off", + {ATTR_ENTITY_ID: "camera.test_cam"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_capture.assert_called_once_with(True) From 1b73bcbff78c510838a98b7cf63fd24c49ff3b9c Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Wed, 29 Jul 2020 07:49:43 +0800 Subject: [PATCH 37/51] Fix songpal already configured check in config flow (#37813) * Fix already configured check * Mark endpoint duplicate check as callback --- homeassistant/components/songpal/config_flow.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index 96e1e7ed7df..9acbedd11c7 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import callback from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import @@ -74,7 +75,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_init(self, user_input=None): """Handle a flow start.""" # Check if already configured - if self._endpoint_already_configured(): + if self._async_endpoint_already_configured(): return self.async_abort(reason="already_configured") if user_input is None: @@ -145,9 +146,10 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_init(user_input) - def _endpoint_already_configured(self): + @callback + def _async_endpoint_already_configured(self): """See if we already have an endpoint matching user input configured.""" - existing_endpoints = [ - entry.data[CONF_ENDPOINT] for entry in self._async_current_entries() - ] - return self.conf.endpoint in existing_endpoints + for entry in self._async_current_entries(): + if entry.data.get(CONF_ENDPOINT) == self.conf.endpoint: + return True + return False From d1fbcba7b6fe8ac8103597b52a8dd5e66946ef9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jul 2020 19:43:42 -1000 Subject: [PATCH 38/51] Prevent kodi from blocking startup (#38257) * Prevent kodi from blocking startup * Update homeassistant/components/kodi/media_player.py * isort * ignore args * adjustments per review * asyncio --- homeassistant/components/kodi/media_player.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index ac31716b887..81f8696a31a 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -1,5 +1,7 @@ """Support for interfacing with the XBMC/Kodi JSON-RPC API.""" +import asyncio from collections import OrderedDict +from datetime import timedelta from functools import wraps import logging import re @@ -53,6 +55,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, script from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template import homeassistant.util.dt as dt_util from homeassistant.util.yaml import dump @@ -82,6 +85,8 @@ DEPRECATED_TURN_OFF_ACTIONS = { "shutdown": "System.Shutdown", } +WEBSOCKET_WATCHDOG_INTERVAL = timedelta(minutes=3) + # https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h MEDIA_TYPES = { "music": MEDIA_TYPE_MUSIC, @@ -435,6 +440,26 @@ class KodiDevice(MediaPlayerEntity): # run until the websocket connection is closed. self.hass.loop.create_task(ws_loop_wrapper()) + async def async_added_to_hass(self): + """Connect the websocket if needed.""" + if not self._enable_websocket: + return + + asyncio.create_task(self.async_ws_connect()) + + self.async_on_remove( + async_track_time_interval( + self.hass, + self._async_connect_websocket_if_disconnected, + WEBSOCKET_WATCHDOG_INTERVAL, + ) + ) + + async def _async_connect_websocket_if_disconnected(self, *_): + """Reconnect the websocket if it fails.""" + if not self._ws_server.connected: + await self.async_ws_connect() + async def async_update(self): """Retrieve latest state.""" self._players = await self._get_players() @@ -445,9 +470,6 @@ class KodiDevice(MediaPlayerEntity): self._app_properties = {} return - if self._enable_websocket and not self._ws_server.connected: - self.hass.async_create_task(self.async_ws_connect()) - self._app_properties = await self.server.Application.GetProperties( ["volume", "muted"] ) From d65545964b8a2a9891475457351e4d9ed836c04b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 29 Jul 2020 13:56:32 -0500 Subject: [PATCH 39/51] Ignore remote Plex clients during plex.tv lookup (#38327) --- homeassistant/components/plex/server.py | 2 +- tests/components/plex/mock_classes.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index db779d67bb0..146291bdbcf 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -115,7 +115,7 @@ class PlexServer: self._plextv_clients = [ x for x in self.account.resources() - if "player" in x.provides and x.presence + if "player" in x.provides and x.presence and x.publicAddressMatches ] _LOGGER.debug( "Current available clients from plex.tv: %s", self._plextv_clients diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index eacee6d9f98..dd8e9a93ab8 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -53,6 +53,7 @@ class MockResource: self.provides = ["player"] self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10) self.presence = index == 0 + self.publicAddressMatches = True def connect(self, timeout): """Mock the resource connect method.""" From dd4e0511a3254d21d03f91953943c5e7bb15c05e Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 29 Jul 2020 08:16:24 -0700 Subject: [PATCH 40/51] Bump androidtv to 0.0.47 and adb-shell to 0.2.1 (#38344) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 40e7575bbb9..7a76a618756 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.2.0", - "androidtv[async]==0.0.46", + "adb-shell[async]==0.2.1", + "androidtv[async]==0.0.47", "pure-python-adb==0.2.2.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index bf08943341d..3306f6ca887 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-circuitpython-bmp280==3.1.1 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.androidtv -adb-shell[async]==0.2.0 +adb-shell[async]==0.2.1 # homeassistant.components.alarmdecoder adext==0.3 @@ -231,7 +231,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.46 +androidtv[async]==0.0.47 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bdf6919659..0a8977d2a7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ YesssSMS==0.4.1 abodepy==1.1.0 # homeassistant.components.androidtv -adb-shell[async]==0.2.0 +adb-shell[async]==0.2.1 # homeassistant.components.adguard adguardhome==0.4.2 @@ -132,7 +132,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.46 +androidtv[async]==0.0.47 # homeassistant.components.apns apns2==0.3.0 From abad6dfdd7720cf7fd6b78e7da2d274901990763 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jul 2020 23:46:14 +0200 Subject: [PATCH 41/51] Bump pychromecast to 7.2.0 (#38351) --- homeassistant/components/cast/config_flow.py | 13 +++++++++++-- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/test_init.py | 12 +++++------- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index 5c2b6dca932..80d4abc9796 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,16 +1,25 @@ """Config flow for Cast.""" -from pychromecast.discovery import discover_chromecasts +import functools + +from pychromecast.discovery import discover_chromecasts, stop_discovery from homeassistant import config_entries from homeassistant.helpers import config_entry_flow from .const import DOMAIN +from .helpers import ChromeCastZeroconf async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - return await hass.async_add_executor_job(discover_chromecasts) + casts, browser = await hass.async_add_executor_job( + functools.partial( + discover_chromecasts, zeroconf_instance=ChromeCastZeroconf.get_zeroconf() + ) + ) + stop_discovery(browser) + return casts config_entry_flow.register_discovery_flow( diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 5d807525226..1187887e864 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.1.2"], + "requirements": ["pychromecast==7.2.0"], "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 3306f6ca887..200f4591d42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.1.2 +pychromecast==7.2.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a8977d2a7a..f2092c69ffd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -577,7 +577,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==7.1.2 +pychromecast==7.2.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 8f194668e56..24be4d53ee6 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -13,7 +13,9 @@ async def test_creating_entry_sets_up_media_player(hass): "homeassistant.components.cast.media_player.async_setup_entry", return_value=True, ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=True + "pychromecast.discovery.discover_chromecasts", return_value=(True, None) + ), patch( + "pychromecast.discovery.stop_discovery" ): result = await hass.config_entries.flow.async_init( cast.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -34,9 +36,7 @@ async def test_configuring_cast_creates_entry(hass): """Test that specifying config will create an entry.""" with patch( "homeassistant.components.cast.async_setup_entry", return_value=True - ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=True - ): + ) as mock_setup: await async_setup_component( hass, cast.DOMAIN, {"cast": {"some_config": "to_trigger_import"}} ) @@ -49,9 +49,7 @@ async def test_not_configuring_cast_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch( "homeassistant.components.cast.async_setup_entry", return_value=True - ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=True - ): + ) as mock_setup: await async_setup_component(hass, cast.DOMAIN, {}) await hass.async_block_till_done() From 9819d70941ebba48f763a17589a972f2f8053797 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 29 Jul 2020 11:49:13 -0600 Subject: [PATCH 42/51] Update aioharmony to 0.2.6 (#38360) --- homeassistant/components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index 40f88ad19ef..4d8b83f4643 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -2,7 +2,7 @@ "domain": "harmony", "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": ["aioharmony==0.2.5"], + "requirements": ["aioharmony==0.2.6"], "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 200f4591d42..f8fe07d1712 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioftp==0.12.0 aioguardian==1.0.1 # homeassistant.components.harmony -aioharmony==0.2.5 +aioharmony==0.2.6 # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.45 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2092c69ffd..359ff4ce433 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -89,7 +89,7 @@ aiofreepybox==0.0.8 aioguardian==1.0.1 # homeassistant.components.harmony -aioharmony==0.2.5 +aioharmony==0.2.6 # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.45 From 0ecaab1a7dbe1fddb461e50a10604e7541ebd666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jul 2020 09:20:06 -1000 Subject: [PATCH 43/51] Avoid error with ignored harmony config entries (#38367) --- homeassistant/components/harmony/config_flow.py | 3 +++ tests/components/harmony/test_config_flow.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 9142eed2dba..6d5adabe235 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -164,6 +164,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _host_already_configured(self, host): """See if we already have a harmony entry matching the host.""" for entry in self._async_current_entries(): + if CONF_HOST not in entry.data: + continue + if entry.data[CONF_HOST] == host: return True return False diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index d159a0f025f..acf1ffd16f1 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -153,6 +153,9 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): ) config_entry.add_to_hass(hass) + config_entry_without_host = MockConfigEntry(domain=DOMAIN, data={"name": "other"},) + config_entry_without_host.add_to_hass(hass) + harmonyapi = _get_mock_harmonyapi(connect=True) with patch( From 293655f9887bd48368a2ee497cb1d3690524f91b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jul 2020 11:20:19 -1000 Subject: [PATCH 44/51] Prevent nut config flow error when checking ignored entries (#38372) --- homeassistant/components/nut/config_flow.py | 1 + tests/components/nut/test_config_flow.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 5d90d16f157..7cebf0c6759 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -216,6 +216,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): existing_host_port_aliases = { _format_host_port_alias(entry.data) for entry in self._async_current_entries() + if CONF_HOST in entry.data } return _format_host_port_alias(user_input) in existing_host_port_aliases diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 5a2155441b5..d86c0d8eb88 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -247,7 +247,25 @@ async def test_form_import_dupe(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=VALID_CONFIG + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_import_with_ignored_entry(hass): + """Test we get abort on duplicate import when there is an ignored one.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG) + entry.add_to_hass(hass) + ignored_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) + ignored_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG ) assert result["type"] == "abort" assert result["reason"] == "already_configured" From 47a729495dcaae63beccdc91a3a2c73f5154bea9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jul 2020 21:37:34 +0200 Subject: [PATCH 45/51] Ensure Toon webhook ID isn't registered on re-registration (#38376) --- homeassistant/components/toon/coordinator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 8e9722316e2..640fa9bb04e 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -71,6 +71,9 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator): self.entry.data[CONF_WEBHOOK_ID] ) + # Ensure the webhook is not registered already + webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID]) + webhook_register( self.hass, DOMAIN, From 2a3947f7cc0906c3034615c6b696459d6fb3ebe6 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 31 Jul 2020 14:38:49 +0200 Subject: [PATCH 46/51] Fix rmvtransport breaking when destinations don't match (#38401) --- .../components/rmvtransport/sensor.py | 34 ++++++++++++++++--- tests/components/rmvtransport/test_sensor.py | 8 ++--- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 704bde67a5c..76e75d77a58 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -100,7 +100,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tasks = [sensor.async_update() for sensor in sensors] if tasks: await asyncio.wait(tasks) - if not all(sensor.data.departures for sensor in sensors): + + if not any(sensor.data for sensor in sensors): raise PlatformNotReady async_add_entities(sensors) @@ -165,6 +166,7 @@ class RMVDepartureSensor(Entity): "minutes": self.data.departures[0].get("minutes"), "departure_time": self.data.departures[0].get("departure_time"), "product": self.data.departures[0].get("product"), + ATTR_ATTRIBUTION: ATTRIBUTION, } except IndexError: return {} @@ -183,13 +185,16 @@ class RMVDepartureSensor(Entity): """Get the latest data and update the state.""" await self.data.async_update() + if self._name == DEFAULT_NAME: + self._name = self.data.station + + self._station = self.data.station + if not self.data.departures: self._state = None self._icon = ICONS[None] return - if self._name == DEFAULT_NAME: - self._name = self.data.station - self._station = self.data.station + self._state = self.data.departures[0].get("minutes") self._icon = ICONS[self.data.departures[0].get("product")] @@ -220,6 +225,7 @@ class RMVDepartureData: self._max_journeys = max_journeys self.rmv = RMVtransport(session, timeout) self.departures = [] + self._error_notification = False @Throttle(SCAN_INTERVAL) async def async_update(self): @@ -231,31 +237,49 @@ class RMVDepartureData: direction_id=self._direction, max_journeys=50, ) + except RMVtransportApiConnectionError: self.departures = [] _LOGGER.warning("Could not retrieve data from rmv.de") return + self.station = _data.get("station") + _deps = [] + _deps_not_found = set(self._destinations) + for journey in _data["journeys"]: # find the first departure meeting the criteria - _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} + _nextdep = {} if self._destinations: dest_found = False for dest in self._destinations: if dest in journey["stops"]: dest_found = True + if dest in _deps_not_found: + _deps_not_found.remove(dest) _nextdep["destination"] = dest + if not dest_found: continue + elif self._lines and journey["number"] not in self._lines: continue + elif journey["minutes"] < self._time_offset: continue + for attr in ["direction", "departure_time", "product", "minutes"]: _nextdep[attr] = journey.get(attr, "") + _nextdep["line"] = journey.get("number", "") _deps.append(_nextdep) + if len(_deps) > self._max_journeys: break + + if not self._error_notification and _deps_not_found: + self._error_notification = True + _LOGGER.info("Destination(s) %s not found", ", ".join(_deps_not_found)) + self.departures = _deps diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 419eaafc5eb..b576f385173 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -48,7 +48,7 @@ VALID_CONFIG_DEST = { def get_departures_mock(): """Mock rmvtransport departures loading.""" - data = { + return { "station": "Frankfurt (Main) Hauptbahnhof", "stationId": "3000010", "filter": "11111111111", @@ -145,18 +145,16 @@ def get_departures_mock(): }, ], } - return data def get_no_departures_mock(): """Mock no departures in results.""" - data = { + return { "station": "Frankfurt (Main) Hauptbahnhof", "stationId": "3000010", "filter": "11111111111", "journeys": [], } - return data async def test_rmvtransport_min_config(hass): @@ -232,4 +230,4 @@ async def test_rmvtransport_no_departures(hass): await hass.async_block_till_done() state = hass.states.get("sensor.frankfurt_main_hauptbahnhof") - assert not state + assert state.state == "unavailable" From f4afa2dc689dfd4e42aa2675508713a3bc1dbfe0 Mon Sep 17 00:00:00 2001 From: Stefan Lehmann Date: Fri, 31 Jul 2020 13:59:32 +0200 Subject: [PATCH 47/51] Fix ads integration after 0.113 (#38402) --- homeassistant/components/ads/__init__.py | 20 +++++++++++++------- homeassistant/components/ads/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 15d58eb4620..b17a066eba7 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -230,7 +230,13 @@ class AdsHub: hnotify = int(contents.hNotification) _LOGGER.debug("Received notification %d", hnotify) - data = contents.data + + # get dynamically sized data array + data_size = contents.cbSampleSize + data = (ctypes.c_ubyte * data_size).from_address( + ctypes.addressof(contents) + + pyads.structs.SAdsNotificationHeader.data.offset + ) try: with self._lock: @@ -241,17 +247,17 @@ class AdsHub: # Parse data to desired datatype if notification_item.plc_datatype == self.PLCTYPE_BOOL: - value = bool(struct.unpack(" Date: Fri, 31 Jul 2020 22:06:02 +0200 Subject: [PATCH 48/51] Pin yarl dependency to 1.4.2 as core dependency (#38428) --- homeassistant/package_constraints.txt | 1 + requirements.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 50e006c3d71..7642ef3480f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,6 +27,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.18 voluptuous-serialize==2.4.0 voluptuous==0.11.7 +yarl==1.4.2 zeroconf==0.27.1 pycryptodome>=3.6.6 diff --git a/requirements.txt b/requirements.txt index 93a95112658..efae1204e11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ requests==2.24.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 voluptuous-serialize==2.4.0 +yarl==1.4.2 diff --git a/setup.py b/setup.py index c2042ab2459..7cf06942f32 100755 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ REQUIRES = [ "ruamel.yaml==0.15.100", "voluptuous==0.11.7", "voluptuous-serialize==2.4.0", + "yarl==1.4.2", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) From 82b3fe1ab62771f5021dded75be3c451962bd7f9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 31 Jul 2020 22:06:17 +0200 Subject: [PATCH 49/51] Fix double encoding issue in google_translate TTS (#38429) --- homeassistant/components/google_translate/tts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 36543e0515e..a040da0dccd 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -8,7 +8,6 @@ from aiohttp.hdrs import REFERER, USER_AGENT import async_timeout from gtts_token import gtts_token import voluptuous as vol -import yarl from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.const import HTTP_OK @@ -129,7 +128,7 @@ class GoogleProvider(Provider): url_param = { "ie": "UTF-8", "tl": language, - "q": yarl.URL(part).raw_path, + "q": part, "tk": part_token, "total": len(message_parts), "idx": idx, From b209c1a7b5d970550deb5289081930c975f95098 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Aug 2020 02:02:48 +0000 Subject: [PATCH 50/51] Bumped version to 0.113.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c5877c5e7b..e4c5b66526c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From c4fcd8bd2e47b8456ef8673baaf590773a57f31e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Jul 2020 15:31:29 +0200 Subject: [PATCH 51/51] Temporary lock pip to 20.1.1 to avoid build issue (#38358) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a17a4dc318f..6a7708c1c5c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,7 +46,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U pip==20.1.1 setuptools pip install -r requirements.txt -r requirements_test.txt # Uninstalling typing as a workaround. Eventually we should make sure # all our dependencies drop typing. @@ -603,7 +603,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools wheel + pip install -U pip==20.1.1 setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt # Uninstalling typing as a workaround. Eventually we should make sure