From e979b5441389ad5510e9097afeb314c4d19d7ac2 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Wed, 20 May 2020 14:53:01 +0200 Subject: [PATCH 01/68] Fix Delijn sensor naming (#35789) --- homeassistant/components/delijn/manifest.json | 2 +- homeassistant/components/delijn/sensor.py | 48 +++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 3f6efd0a3d7..8727dd25139 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -3,5 +3,5 @@ "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", "codeowners": ["@bollewolle"], - "requirements": ["pydelijn==0.5.1"] + "requirements": ["pydelijn==0.6.0"] } diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 538e071e194..2c7eec1691c 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -2,6 +2,7 @@ import logging from pydelijn.api import Passages +from pydelijn.common import HttpException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -37,22 +38,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the sensor.""" api_key = config[CONF_API_KEY] - name = DEFAULT_NAME session = async_get_clientsession(hass) sensors = [] for nextpassage in config[CONF_NEXT_DEPARTURE]: - stop_id = nextpassage[CONF_STOP_ID] - number_of_departures = nextpassage[CONF_NUMBER_OF_DEPARTURES] - line = Passages( - hass.loop, stop_id, number_of_departures, api_key, session, True + sensors.append( + DeLijnPublicTransportSensor( + Passages( + hass.loop, + nextpassage[CONF_STOP_ID], + nextpassage[CONF_NUMBER_OF_DEPARTURES], + api_key, + session, + True, + ) + ) ) - await line.get_passages() - if line.passages is None: - _LOGGER.warning("No data received from De Lijn") - return - sensors.append(DeLijnPublicTransportSensor(line, name)) async_add_entities(sensors, True) @@ -60,20 +62,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class DeLijnPublicTransportSensor(Entity): """Representation of a Ruter sensor.""" - def __init__(self, line, name): + def __init__(self, line): """Initialize the sensor.""" self.line = line self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._name = name + self._name = None self._state = None - self._available = False + self._available = True async def async_update(self): """Get the latest data from the De Lijn API.""" - await self.line.get_passages() - if self.line.passages is None: - _LOGGER.warning("No data received from De Lijn") + try: + await self.line.get_passages() + self._name = await self.line.get_stopname() + except HttpException: + self._available = False + _LOGGER.error("De Lijn http error") return + + self._attributes["stopname"] = self._name + for passage in self.line.passages: + passage["stopname"] = self._name + try: first = self.line.passages[0] if first["due_at_realtime"] is not None: @@ -81,8 +91,6 @@ class DeLijnPublicTransportSensor(Entity): else: first_passage = first["due_at_schedule"] self._state = first_passage - self._name = first["stopname"] - self._attributes["stopname"] = first["stopname"] self._attributes["line_number_public"] = first["line_number_public"] self._attributes["line_transport_type"] = first["line_transport_type"] self._attributes["final_destination"] = first["final_destination"] @@ -90,8 +98,8 @@ class DeLijnPublicTransportSensor(Entity): self._attributes["due_at_realtime"] = first["due_at_realtime"] self._attributes["next_passages"] = self.line.passages self._available = True - except (KeyError, IndexError) as error: - _LOGGER.debug("Error getting data from De Lijn: %s", error) + except (KeyError, IndexError): + _LOGGER.error("Invalid data received from De Lijn") self._available = False @property diff --git a/requirements_all.txt b/requirements_all.txt index f51773815dc..eecf3ef956e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1272,7 +1272,7 @@ pydanfossair==0.1.0 pydeconz==70 # homeassistant.components.delijn -pydelijn==0.5.1 +pydelijn==0.6.0 # homeassistant.components.zwave pydispatcher==2.0.5 From 2d76e12c21f81db9b1919baa02bea157468d40e8 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 21 May 2020 03:06:51 +0800 Subject: [PATCH 02/68] Handle None received from pyforked-daapd (#35830) * Handle None received from API in forked-daapd * Bump pyforked-daapd version in requirements * Add test --- .../components/forked_daapd/manifest.json | 2 +- .../components/forked_daapd/media_player.py | 62 ++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../forked_daapd/test_media_player.py | 31 +++++++++- 5 files changed, 66 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index ee57f678601..171776a290a 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -3,7 +3,7 @@ "name": "forked-daapd", "documentation": "https://www.home-assistant.io/integrations/forked-daapd", "codeowners": ["@uvjustin"], - "requirements": ["pyforked-daapd==0.1.8", "pylibrespot-java==0.1.0"], + "requirements": ["pyforked-daapd==0.1.9", "pylibrespot-java==0.1.0"], "config_flow": true, "zeroconf": ["_daap._tcp.local."] } diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index e492aa1b454..07cc11807fd 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -794,26 +794,29 @@ class ForkedDaapdUpdater: "queue" in update_types ): # update queue, queue before player for async_play_media queue = await self._api.get_request("queue") - update_events["queue"] = asyncio.Event() - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_QUEUE.format(self._entry_id), - queue, - update_events["queue"], - ) + if queue: + update_events["queue"] = asyncio.Event() + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_QUEUE.format(self._entry_id), + queue, + update_events["queue"], + ) # order of below don't matter if not {"outputs", "volume"}.isdisjoint(update_types): # update outputs - outputs = (await self._api.get_request("outputs"))["outputs"] - update_events[ - "outputs" - ] = asyncio.Event() # only for master, zones should ignore - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), - outputs, - update_events["outputs"], - ) - self._add_zones(outputs) + outputs = await self._api.get_request("outputs") + if outputs: + outputs = outputs["outputs"] + update_events[ + "outputs" + ] = asyncio.Event() # only for master, zones should ignore + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), + outputs, + update_events["outputs"], + ) + self._add_zones(outputs) if not {"database"}.isdisjoint(update_types): pipes, playlists = await asyncio.gather( self._api.get_pipes(), self._api.get_playlists() @@ -832,17 +835,18 @@ class ForkedDaapdUpdater: update_types ): # update player player = await self._api.get_request("player") - update_events["player"] = asyncio.Event() - if update_events.get("queue"): - await update_events[ - "queue" - ].wait() # make sure queue done before player for async_play_media - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_PLAYER.format(self._entry_id), - player, - update_events["player"], - ) + if player: + update_events["player"] = asyncio.Event() + if update_events.get("queue"): + await update_events[ + "queue" + ].wait() # make sure queue done before player for async_play_media + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_PLAYER.format(self._entry_id), + player, + update_events["player"], + ) if update_events: await asyncio.wait( [event.wait() for event in update_events.values()] diff --git a/requirements_all.txt b/requirements_all.txt index eecf3ef956e..fda4c3435b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1332,7 +1332,7 @@ pyflunearyou==1.0.7 pyfnip==0.2 # homeassistant.components.forked_daapd -pyforked-daapd==0.1.8 +pyforked-daapd==0.1.9 # homeassistant.components.fritzbox pyfritzhome==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7748649a36..89f8c084907 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -554,7 +554,7 @@ pyflume==0.4.0 pyflunearyou==1.0.7 # homeassistant.components.forked_daapd -pyforked-daapd==0.1.8 +pyforked-daapd==0.1.9 # homeassistant.components.fritzbox pyfritzhome==0.4.2 diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 8f3a6c2c139..43a39146199 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -8,6 +8,9 @@ from homeassistant.components.forked_daapd.const import ( CONF_TTS_PAUSE_TIME, CONF_TTS_VOLUME, DOMAIN, + SIGNAL_UPDATE_OUTPUTS, + SIGNAL_UPDATE_PLAYER, + SIGNAL_UPDATE_QUEUE, SOURCE_NAME_CLEAR, SOURCE_NAME_DEFAULT, SUPPORTED_FEATURES, @@ -63,7 +66,7 @@ from homeassistant.const import ( ) from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_mock_signal TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" TEST_ZONE_ENTITY_NAMES = [ @@ -369,6 +372,32 @@ def test_master_state(hass, mock_api_object): assert not state.attributes[ATTR_MEDIA_SHUFFLE] +async def test_no_update_when_get_request_returns_none( + hass, config_entry, mock_api_object +): + """Test when get request returns None.""" + + async def get_request_side_effect(update_type): + return None + + mock_api_object.get_request.side_effect = get_request_side_effect + updater_update = mock_api_object.start_websocket_handler.call_args[0][2] + signal_output_call = async_mock_signal( + hass, SIGNAL_UPDATE_OUTPUTS.format(config_entry.entry_id) + ) + signal_player_call = async_mock_signal( + hass, SIGNAL_UPDATE_PLAYER.format(config_entry.entry_id) + ) + signal_queue_call = async_mock_signal( + hass, SIGNAL_UPDATE_QUEUE.format(config_entry.entry_id) + ) + await updater_update(["outputs", "player", "queue"]) + await hass.async_block_till_done() + assert len(signal_output_call) == 0 + assert len(signal_player_call) == 0 + assert len(signal_queue_call) == 0 + + async def _service_call( hass, entity_name, service, additional_service_data=None, blocking=True ): From 813203a38f797a312ab6a0031ad987cd875f8245 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 20 May 2020 13:25:42 +0200 Subject: [PATCH 03/68] Fix Daikin duplicate entries (#35833) --- homeassistant/components/daikin/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 1e03938dfcf..06735d7e3b8 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from . import config_flow # noqa: F401 -from .const import CONF_KEY, CONF_UUID, TIMEOUT +from .const import CONF_KEY, CONF_UUID, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -61,6 +61,9 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Establish connection with Daikin.""" conf = entry.data + # For backwards compat, set unique ID + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=conf[KEY_MAC]) daikin_api = await daikin_api_setup( hass, conf[CONF_HOST], From 1c38bcaeb52d4ad4c7847ea58abb29a7b14e6dea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 May 2020 03:30:18 -0500 Subject: [PATCH 04/68] Homekit should skip devices that are missing in device registry (#35857) * Homekit should skip devices that are missing in device registry * Add test for this failure state --- homeassistant/components/homekit/__init__.py | 14 ++-- tests/components/homekit/test_homekit.py | 80 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 0a208e012fb..adbf79128e3 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -627,12 +627,14 @@ class HomeKit: ent_cfg = self._config.setdefault(entity_id, {}) if ent_reg_ent.device_id: dev_reg_ent = dev_reg.async_get(ent_reg_ent.device_id) - if dev_reg_ent.manufacturer: - ent_cfg[ATTR_MANUFACTURER] = dev_reg_ent.manufacturer - if dev_reg_ent.model: - ent_cfg[ATTR_MODEL] = dev_reg_ent.model - if dev_reg_ent.sw_version: - ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version + if dev_reg_ent is not None: + # Handle missing devices + if dev_reg_ent.manufacturer: + ent_cfg[ATTR_MANUFACTURER] = dev_reg_ent.manufacturer + if dev_reg_ent.model: + ent_cfg[ATTR_MODEL] = dev_reg_ent.model + if dev_reg_ent.sw_version: + ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version if ATTR_MANUFACTURER not in ent_cfg: integration = await async_get_integration(self.hass, ent_reg_ent.platform) ent_cfg[ATTR_INTERGRATION] = integration.name diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 8a4ac87f21b..c0e2ea90fba 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -913,3 +913,83 @@ def _write_data(path: str, data: Dict) -> None: if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) json_util.save_json(path, data) + + +async def test_homekit_ignored_missing_devices( + hass, hk_driver, debounce_patcher, device_reg, entity_reg +): + """Test HomeKit handles a device in the entity registry but missing from the device registry.""" + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {"light.demo": {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) + homekit.driver = hk_driver + homekit._filter = Mock(return_value=True) + homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + sw_version="0.16.0", + model="Powerwall 2", + manufacturer="Tesla", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + entity_reg.async_get_or_create( + "binary_sensor", + "powerwall", + "battery_charging", + device_id=device_entry.id, + device_class=DEVICE_CLASS_BATTERY_CHARGING, + ) + entity_reg.async_get_or_create( + "sensor", + "powerwall", + "battery", + device_id=device_entry.id, + device_class=DEVICE_CLASS_BATTERY, + ) + light = entity_reg.async_get_or_create( + "light", "powerwall", "demo", device_id=device_entry.id + ) + + # Delete the device to make sure we fallback + # to using the platform + device_reg.async_remove_device(device_entry.id) + + hass.states.async_set(light.entity_id, STATE_ON) + + def _mock_get_accessory(*args, **kwargs): + return [None, "acc", None] + + with patch.object(homekit.bridge, "add_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( + "pyhap.accessory_driver.AccessoryDriver.start" + ): + await homekit.async_start() + await hass.async_block_till_done() + + mock_get_acc.assert_called_with( + hass, + hk_driver, + ANY, + ANY, + { + "platform": "Tesla Powerwall", + "linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging", + "linked_battery_sensor": "sensor.powerwall_battery", + }, + ) From b9ab6d1e10d1c3eb211575114e4d75f40140e100 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 May 2020 10:25:28 +0200 Subject: [PATCH 05/68] Updated frontend to 20200519.1 (#35877) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 563e71c2eec..01d1b6eb88f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200519.0"], + "requirements": ["home-assistant-frontend==20200519.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c59502846b6..d356e62a32e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 hass-nabucasa==0.34.2 -home-assistant-frontend==20200519.0 +home-assistant-frontend==20200519.1 importlib-metadata==1.6.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index fda4c3435b7..0b8d15022b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.0 +home-assistant-frontend==20200519.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89f8c084907..570a59cc780 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.0 +home-assistant-frontend==20200519.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c7008eb7615681b40448aacf87b91f038fad7e89 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 21 May 2020 10:35:04 +0200 Subject: [PATCH 06/68] Fix light profiles for HomeMatic lights (#35882) --- homeassistant/components/homematic/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index c7cfcc2ac8c..7ef632dae81 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -111,7 +111,7 @@ class HMLight(HMDevice, LightEntity): ): self._hmdevice.on(self._channel) - if ATTR_HS_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs and self.supported_features & SUPPORT_COLOR: self._hmdevice.set_hs_color( hue=kwargs[ATTR_HS_COLOR][0] / 360.0, saturation=kwargs[ATTR_HS_COLOR][1] / 100.0, From ae66f4250c9b2e426abfdc8b0dd66a032c1640b6 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 20 May 2020 21:26:27 -0400 Subject: [PATCH 07/68] fix mjpeg issue along with some cameras not returning event capabilities properly (#35885) --- homeassistant/components/onvif/camera.py | 20 ++++++++++---------- homeassistant/components/onvif/device.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 4d39c95c3cd..570b99bfe3a 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -106,11 +106,6 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): async def stream_source(self): """Return the stream source.""" - if self._stream_uri is None: - uri_no_auth = await self.device.async_get_stream_uri(self.profile) - self._stream_uri = uri_no_auth.replace( - "rtsp://", f"rtsp://{self.device.username}:{self.device.password}@", 1 - ) return self._stream_uri async def async_camera_image(self): @@ -118,11 +113,6 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): image = None if self.device.capabilities.snapshot: - if self._snapshot_uri is None: - self._snapshot_uri = await self.device.async_get_snapshot_uri( - self.profile - ) - auth = None if self.device.username and self.device.password: auth = HTTPDigestAuth(self.device.username, self.device.password) @@ -181,6 +171,16 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): finally: await stream.close() + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + uri_no_auth = await self.device.async_get_stream_uri(self.profile) + self._stream_uri = uri_no_auth.replace( + "rtsp://", f"rtsp://{self.device.username}:{self.device.password}@", 1 + ) + + if self.device.capabilities.snapshot: + self._snapshot_uri = await self.device.async_get_snapshot_uri(self.profile) + async def async_perform_ptz( self, distance, diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 8e69e148da3..0a35dadec26 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -223,7 +223,7 @@ class ONVIFDevice: try: media_service = self.device.create_media_service() media_capabilities = await media_service.GetServiceCapabilities() - snapshot = media_capabilities.SnapshotUri + snapshot = media_capabilities and media_capabilities.SnapshotUri except (ONVIFError, Fault): pass @@ -231,7 +231,7 @@ class ONVIFDevice: try: event_service = self.device.create_events_service() event_capabilities = await event_service.GetServiceCapabilities() - pullpoint = event_capabilities.WSPullPointSupport + pullpoint = event_capabilities and event_capabilities.WSPullPointSupport except (ONVIFError, Fault): pass From c88c011df9135f02d09e69dcd2d77204daa33050 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 May 2020 01:48:01 -0500 Subject: [PATCH 08/68] Ensure http can startup if homekit fails to load (#35888) * Ensure HomeAssistant can startup if homekit fails to load * Update homeassistant/components/logbook/manifest.json Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/logbook/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 3980469c9c5..26586013108 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,6 +3,5 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "after_dependencies": ["homekit"], "codeowners": [] } From ddee8b68c65ba3ec56b43967fbd21af91f40f0e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 May 2020 03:27:40 -0500 Subject: [PATCH 09/68] Ensure storage write consume the data under the lock (#35889) If two writes trigger at the same time the data would already be consumed. --- homeassistant/helpers/storage.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index b5ac942bf2f..d2b4c334937 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -200,14 +200,19 @@ class Store: async def _async_handle_write_data(self, *_args): """Handle writing the config.""" - data = self._data - - if "data_func" in data: - data["data"] = data.pop("data_func")() - - self._data = None async with self._write_lock: + if self._data is None: + # Another write already consumed the data + return + + data = self._data + + if "data_func" in data: + data["data"] = data.pop("data_func")() + + self._data = None + try: await self.hass.async_add_executor_job( self._write_data, self.path, data From e170e6563b903cf246aff70390e9ab50ad6ffd55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 May 2020 02:18:45 -0500 Subject: [PATCH 10/68] Fix legacy Hunter Douglas PowerView devices (#35895) These devices are missing firmware information as the 1.0 firmware did not provide it. --- .../hunterdouglas_powerview/__init__.py | 22 ++++++++++-- .../hunterdouglas_powerview/const.py | 5 +++ .../hunterdouglas_powerview/userdata_v1.json | 34 +++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/hunterdouglas_powerview/userdata_v1.json diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 89dc610a6fc..df06060fb75 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -31,9 +31,15 @@ from .const import ( DEVICE_REVISION, DEVICE_SERIAL_NUMBER, DOMAIN, + FIRMWARE_BUILD, FIRMWARE_IN_USERDATA, + FIRMWARE_SUB_REVISION, HUB_EXCEPTIONS, HUB_NAME, + LEGACY_DEVICE_BUILD, + LEGACY_DEVICE_MODEL, + LEGACY_DEVICE_REVISION, + LEGACY_DEVICE_SUB_REVISION, MAC_ADDRESS_IN_USERDATA, MAINPROCESSOR_IN_USERDATA_FIRMWARE, MODEL_IN_MAINPROCESSOR, @@ -159,9 +165,19 @@ async def async_get_device_info(pv_request): resources = await userdata.get_resources() userdata_data = resources[USER_DATA] - main_processor_info = userdata_data[FIRMWARE_IN_USERDATA][ - MAINPROCESSOR_IN_USERDATA_FIRMWARE - ] + if FIRMWARE_IN_USERDATA in userdata_data: + main_processor_info = userdata_data[FIRMWARE_IN_USERDATA][ + MAINPROCESSOR_IN_USERDATA_FIRMWARE + ] + else: + # Legacy devices + main_processor_info = { + REVISION_IN_MAINPROCESSOR: LEGACY_DEVICE_REVISION, + FIRMWARE_SUB_REVISION: LEGACY_DEVICE_SUB_REVISION, + FIRMWARE_BUILD: LEGACY_DEVICE_BUILD, + MODEL_IN_MAINPROCESSOR: LEGACY_DEVICE_MODEL, + } + return { DEVICE_NAME: base64_to_unicode(userdata_data[HUB_NAME]), DEVICE_MAC_ADDRESS: userdata_data[MAC_ADDRESS_IN_USERDATA], diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index 17ff3821a7a..e69fe319c0f 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -65,3 +65,8 @@ PV_ROOM_DATA = "pv_room_data" COORDINATOR = "coordinator" HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError) + +LEGACY_DEVICE_SUB_REVISION = 1 +LEGACY_DEVICE_REVISION = 0 +LEGACY_DEVICE_BUILD = 0 +LEGACY_DEVICE_MODEL = "PV Hub1.0" diff --git a/tests/fixtures/hunterdouglas_powerview/userdata_v1.json b/tests/fixtures/hunterdouglas_powerview/userdata_v1.json new file mode 100644 index 00000000000..d97aca162f8 --- /dev/null +++ b/tests/fixtures/hunterdouglas_powerview/userdata_v1.json @@ -0,0 +1,34 @@ +{ + "userData" : { + "enableScheduledEvents" : true, + "staticIp" : false, + "sceneControllerCount" : 0, + "accessPointCount" : 0, + "shadeCount" : 5, + "ip" : "192.168.20.9", + "groupCount" : 9, + "scheduledEventCount" : 0, + "editingEnabled" : true, + "roomCount" : 5, + "setupCompleted" : false, + "sceneCount" : 18, + "sceneControllerMemberCount" : 0, + "mask" : "255.255.255.0", + "hubName" : "UG93ZXJWaWV3IEh1YiBHZW4gMQ==", + "rfID" : "0x8B2A", + "remoteConnectEnabled" : false, + "multiSceneMemberCount" : 0, + "rfStatus" : 0, + "serialNumber" : "REMOVED", + "undefinedShadeCount" : 0, + "sceneMemberCount" : 18, + "unassignedShadeCount" : 0, + "multiSceneCount" : 0, + "addressKind" : "newPrimary", + "gateway" : "192.168.20.1", + "localTimeDataSet" : true, + "dns" : "192.168.20.1", + "macAddress" : "00:00:00:00:00:eb", + "rfIDInt" : 35626 + } +} From b266d3aaaefac83490a79d2f18313de4ddb10c34 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 21 May 2020 08:47:50 +0000 Subject: [PATCH 11/68] Bump version 0.110.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e7eddca36e0..366c2c79d99 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 110 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 19f36eb5a19118a987cc7bf2630a03ae8c37b76e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 21 May 2020 02:49:18 -0600 Subject: [PATCH 12/68] Auto-level AirVisual API calls (#34903) --- .../components/airvisual/__init__.py | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 4c46e7b3e7d..e5d8b03f316 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,6 +1,7 @@ """The airvisual component.""" import asyncio from datetime import timedelta +from math import ceil from pyairvisual import Client from pyairvisual.errors import AirVisualError, NodeProError @@ -37,7 +38,6 @@ from .const import ( PLATFORMS = ["air_quality", "sensor"] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" -DEFAULT_GEOGRAPHY_SCAN_INTERVAL = timedelta(minutes=10) DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1) DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True} @@ -88,6 +88,37 @@ def async_get_geography_id(geography_dict): ) +@callback +def async_get_cloud_api_update_interval(hass, api_key): + """Get a leveled scan interval for a particular cloud API key. + + This will shift based on the number of active consumers, thus keeping the user + under the monthly API limit. + """ + num_consumers = len( + { + config_entry + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.data.get(CONF_API_KEY) == api_key + } + ) + + # Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note + # that we give a buffer of 1500 API calls for any drift, restarts, etc.: + minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers)) + return timedelta(minutes=minutes_between_api_calls) + + +@callback +def async_reset_coordinator_update_intervals(hass, update_interval): + """Update any existing data coordinators with a new update interval.""" + if not hass.data[DOMAIN][DATA_COORDINATOR]: + return + + for coordinator in hass.data[DOMAIN][DATA_COORDINATOR].values(): + coordinator.update_interval = update_interval + + async def async_setup(hass, config): """Set up the AirVisual component.""" hass.data[DOMAIN] = {DATA_COORDINATOR: {}} @@ -163,6 +194,10 @@ async def async_setup_entry(hass, config_entry): client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession) + update_interval = async_get_cloud_api_update_interval( + hass, config_entry.data[CONF_API_KEY] + ) + async def async_update_data(): """Get new data from the API.""" if CONF_CITY in config_entry.data: @@ -185,10 +220,14 @@ async def async_setup_entry(hass, config_entry): hass, LOGGER, name="geography data", - update_interval=DEFAULT_GEOGRAPHY_SCAN_INTERVAL, + update_interval=update_interval, update_method=async_update_data, ) + # Ensure any other, existing config entries that use this API key are updated + # with the new scan interval: + async_reset_coordinator_update_intervals(hass, update_interval) + # Only geography-based entries have options: config_entry.add_update_listener(async_update_options) else: From 534fcefae1d5512012866a8d3fdb4f82e5a94d89 Mon Sep 17 00:00:00 2001 From: Robert Chmielowiec Date: Sat, 23 May 2020 18:11:51 +0200 Subject: [PATCH 13/68] Fix service registration supported features check (#35718) --- homeassistant/helpers/service.py | 3 +- tests/helpers/test_service.py | 77 ++++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ce52d188540..af4bdb50fa4 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -431,7 +431,8 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # Skip entities that don't have the required feature. if required_features is not None and not any( - entity.supported_features & feature_set for feature_set in required_features + entity.supported_features & feature_set == feature_set + for feature_set in required_features ): continue diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index e87fd2646dd..ba72cbc83ca 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -35,6 +35,10 @@ from tests.common import ( mock_service, ) +SUPPORT_A = 1 +SUPPORT_B = 2 +SUPPORT_C = 4 + @pytest.fixture def mock_handle_entity_call(): @@ -52,17 +56,31 @@ def mock_entities(hass): entity_id="light.kitchen", available=True, should_poll=False, - supported_features=1, + supported_features=SUPPORT_A, ) living_room = MockEntity( entity_id="light.living_room", available=True, should_poll=False, - supported_features=0, + supported_features=SUPPORT_B, + ) + bedroom = MockEntity( + entity_id="light.bedroom", + available=True, + should_poll=False, + supported_features=(SUPPORT_A | SUPPORT_B), + ) + bathroom = MockEntity( + entity_id="light.bathroom", + available=True, + should_poll=False, + supported_features=(SUPPORT_B | SUPPORT_C), ) entities = OrderedDict() entities[kitchen.entity_id] = kitchen entities[living_room.entity_id] = living_room + entities[bedroom.entity_id] = bedroom + entities[bathroom.entity_id] = bathroom return entities @@ -307,18 +325,61 @@ async def test_async_get_all_descriptions(hass): async def test_call_with_required_features(hass, mock_entities): - """Test service calls invoked only if entity has required feautres.""" + """Test service calls invoked only if entity has required features.""" test_service_mock = AsyncMock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], test_service_mock, ha.ServiceCall("test_domain", "test_service", {"entity_id": "all"}), - required_features=[1], + required_features=[SUPPORT_A], ) - assert len(mock_entities) == 2 - # Called once because only one of the entities had the required features + + assert test_service_mock.call_count == 2 + expected = [ + mock_entities["light.kitchen"], + mock_entities["light.bedroom"], + ] + actual = [call[0][0] for call in test_service_mock.call_args_list] + assert all(entity in actual for entity in expected) + + +async def test_call_with_both_required_features(hass, mock_entities): + """Test service calls invoked only if entity has both features.""" + test_service_mock = AsyncMock(return_value=None) + await service.entity_service_call( + hass, + [Mock(entities=mock_entities)], + test_service_mock, + ha.ServiceCall("test_domain", "test_service", {"entity_id": "all"}), + required_features=[SUPPORT_A | SUPPORT_B], + ) + assert test_service_mock.call_count == 1 + assert [call[0][0] for call in test_service_mock.call_args_list] == [ + mock_entities["light.bedroom"] + ] + + +async def test_call_with_one_of_required_features(hass, mock_entities): + """Test service calls invoked with one entity having the required features.""" + test_service_mock = AsyncMock(return_value=None) + await service.entity_service_call( + hass, + [Mock(entities=mock_entities)], + test_service_mock, + ha.ServiceCall("test_domain", "test_service", {"entity_id": "all"}), + required_features=[SUPPORT_A, SUPPORT_C], + ) + + assert test_service_mock.call_count == 3 + expected = [ + mock_entities["light.kitchen"], + mock_entities["light.bedroom"], + mock_entities["light.bathroom"], + ] + actual = [call[0][0] for call in test_service_mock.call_args_list] + assert all(entity in actual for entity in expected) async def test_call_with_sync_func(hass, mock_entities): @@ -458,7 +519,7 @@ async def test_call_no_context_target_all(hass, mock_handle_entity_call, mock_en ), ) - assert len(mock_handle_entity_call.mock_calls) == 2 + assert len(mock_handle_entity_call.mock_calls) == 4 assert [call[1][1] for call in mock_handle_entity_call.mock_calls] == list( mock_entities.values() ) @@ -494,7 +555,7 @@ async def test_call_with_match_all( ha.ServiceCall("test_domain", "test_service", {"entity_id": "all"}), ) - assert len(mock_handle_entity_call.mock_calls) == 2 + assert len(mock_handle_entity_call.mock_calls) == 4 assert [call[1][1] for call in mock_handle_entity_call.mock_calls] == list( mock_entities.values() ) From 43ec35ee179b03dce948dc7a2a77bbb998c9e295 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 May 2020 16:09:00 -0600 Subject: [PATCH 14/68] Fix Prezzibenzina doing I/O in the event loop (#35881) * Fix Prezzibenzina doing I/O in the event loop * Linting --- homeassistant/components/prezzibenzina/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py index c985f96e6c6..f45d9d84669 100644 --- a/homeassistant/components/prezzibenzina/sensor.py +++ b/homeassistant/components/prezzibenzina/sensor.py @@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PrezziBenzina sensor platform.""" station = config[CONF_STATION] @@ -65,7 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(dev, True) + add_entities(dev, True) class PrezziBenzinaSensor(Entity): @@ -114,6 +114,6 @@ class PrezziBenzinaSensor(Entity): } return attrs - async def async_update(self): + def update(self): """Get the latest data and updates the states.""" self._data = self._client.get_by_id(self._station)[self._index] From 8d008d31bd8edddd125b6e756918541d9780dbb3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 21 May 2020 02:22:49 -0500 Subject: [PATCH 15/68] Update rokuecp to 0.4.1 (#35899) * update rokuecp to 0.4.1 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 62b3cc58fc8..57c64f4c64a 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.4.0"], + "requirements": ["rokuecp==0.4.1"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index 0b8d15022b4..82c99c7e053 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1871,7 +1871,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.4.0 +rokuecp==0.4.1 # homeassistant.components.roomba roombapy==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 570a59cc780..29868bc4004 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ rflink==0.0.52 ring_doorbell==0.6.0 # homeassistant.components.roku -rokuecp==0.4.0 +rokuecp==0.4.1 # homeassistant.components.roomba roombapy==1.6.1 From d299a92cd3d0054308db25d217c2567679397d67 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 May 2020 17:43:30 +0200 Subject: [PATCH 16/68] Update frontend to 20200519.3 (#35925) * Updated frontend to 20200519.2 * Updated frontend to 20200519.3 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 01d1b6eb88f..042ae78535d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200519.1"], + "requirements": ["home-assistant-frontend==20200519.3"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d356e62a32e..b6f19f3e728 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 hass-nabucasa==0.34.2 -home-assistant-frontend==20200519.1 +home-assistant-frontend==20200519.3 importlib-metadata==1.6.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 82c99c7e053..2754a985f36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.1 +home-assistant-frontend==20200519.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29868bc4004..95aceb77f8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.1 +home-assistant-frontend==20200519.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f452e262694adba62bf4e976a407e45da5074637 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 May 2020 19:52:33 -0500 Subject: [PATCH 17/68] Ensure homekit functions if numpy is unavailable (#35931) --- homeassistant/components/homekit/img_util.py | 8 ++++++-- tests/components/homekit/test_img_util.py | 18 +++++------------- tests/components/homekit/test_type_cameras.py | 4 +--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py index 835b04558e6..88217bf776d 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/homekit/img_util.py @@ -2,8 +2,6 @@ import logging -from turbojpeg import TurboJPEG - SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] _LOGGER = logging.getLogger(__name__) @@ -54,6 +52,12 @@ class TurboJPEGSingleton: def __init__(self): """Try to create TurboJPEG only once.""" try: + # TurboJPEG checks for libturbojpeg + # when its created, but it imports + # numpy which may or may not work so + # we have to guard the import here. + from turbojpeg import TurboJPEG # pylint: disable=import-outside-toplevel + TurboJPEGSingleton.__instance = TurboJPEG() except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/tests/components/homekit/test_img_util.py b/tests/components/homekit/test_img_util.py index 4ada89b3acd..728bb8847ff 100644 --- a/tests/components/homekit/test_img_util.py +++ b/tests/components/homekit/test_img_util.py @@ -23,25 +23,19 @@ def test_scale_jpeg_camera_image(): camera_image = Image("image/jpeg", EMPTY_16_12_JPEG) turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) - with patch( - "homeassistant.components.homekit.img_util.TurboJPEG", return_value=False - ): + with patch("turbojpeg.TurboJPEG", return_value=False): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) - with patch( - "homeassistant.components.homekit.img_util.TurboJPEG", return_value=turbo_jpeg - ): + with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == EMPTY_16_12_JPEG turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=8, second_height=6 ) - with patch( - "homeassistant.components.homekit.img_util.TurboJPEG", return_value=turbo_jpeg - ): + with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): TurboJPEGSingleton() jpeg_bytes = scale_jpeg_camera_image(camera_image, 8, 6) @@ -51,12 +45,10 @@ def test_scale_jpeg_camera_image(): def test_turbojpeg_load_failure(): """Handle libjpegturbo not being installed.""" - with patch( - "homeassistant.components.homekit.img_util.TurboJPEG", side_effect=Exception - ): + with patch("turbojpeg.TurboJPEG", side_effect=Exception): TurboJPEGSingleton() assert TurboJPEGSingleton.instance() is False - with patch("homeassistant.components.homekit.img_util.TurboJPEG"): + with patch("turbojpeg.TurboJPEG"): TurboJPEGSingleton() assert TurboJPEGSingleton.instance() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index e3444ca23e4..0c002fa7213 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -193,9 +193,7 @@ async def test_camera_stream_source_configured(hass, run_driver, events): turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=300, second_height=200 ) - with patch( - "homeassistant.components.homekit.img_util.TurboJPEG", return_value=turbo_jpeg - ): + with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): TurboJPEGSingleton() assert await hass.async_add_executor_job( acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} From 8a9ba7e72dc95b96ed2f12db91a262bccbe715f4 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 22 May 2020 19:46:11 -0400 Subject: [PATCH 18/68] Fix ONVIF Transport (#35932) * allow lib to create AsyncTransport * fix transport close issue --- homeassistant/components/onvif/__init__.py | 3 ++- homeassistant/components/onvif/device.py | 16 +++++++++------- homeassistant/components/onvif/event.py | 4 ++-- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 6d90c5828f9..bb8008e1fef 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -82,13 +82,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if device.capabilities.events and await device.events.async_start(): platforms += ["binary_sensor", "sensor"] - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.events.async_stop) for component in platforms: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + return True diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 0a35dadec26..d6f407f016d 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -8,7 +8,6 @@ from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedE import onvif from onvif import ONVIFCamera from onvif.exceptions import ONVIFError -from zeep.asyncio import AsyncTransport from zeep.exceptions import Fault from homeassistant.config_entries import ConfigEntry @@ -20,7 +19,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util from .const import ( @@ -141,6 +139,12 @@ class ONVIFDevice: return True + async def async_stop(self, event=None): + """Shut it all down.""" + if self.events: + await self.events.async_stop() + await self.device.close() + async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" LOGGER.debug("Setting up the ONVIF device management service") @@ -278,7 +282,7 @@ class ONVIFDevice: is not None, ) - ptz_service = self.device.get_service("ptz") + ptz_service = self.device.create_ptz_service() presets = await ptz_service.GetPresets(profile.token) profile.ptz.presets = [preset.token for preset in presets] @@ -326,7 +330,7 @@ class ONVIFDevice: LOGGER.warning("PTZ actions are not supported on device '%s'", self.name) return - ptz_service = self.device.get_service("ptz") + ptz_service = self.device.create_ptz_service() pan_val = distance * PAN_FACTOR.get(pan, 0) tilt_val = distance * TILT_FACTOR.get(tilt, 0) @@ -423,13 +427,11 @@ class ONVIFDevice: def get_device(hass, host, port, username, password) -> ONVIFCamera: """Get ONVIFCamera instance.""" - session = async_get_clientsession(hass) - transport = AsyncTransport(None, session=session) return ONVIFCamera( host, port, username, password, f"{os.path.dirname(onvif.__file__)}/wsdl/", - transport=transport, + no_cache=True, ) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 888fe5bd92b..183ad0ab532 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -91,7 +91,7 @@ class EventManager: return self.started - async def async_stop(self, event=None) -> None: + async def async_stop(self) -> None: """Unsubscribe from events.""" if not self._subscription: return @@ -110,7 +110,7 @@ class EventManager: async def async_pull_messages(self, _now: dt = None) -> None: """Pull messages from device.""" try: - pullpoint = self.device.get_service("pullpoint") + pullpoint = self.device.create_pullpoint_service() req = pullpoint.create_type("PullMessages") req.MessageLimit = 100 req.Timeout = dt.timedelta(seconds=60) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index f291f9c6613..4214cf3ab5c 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,7 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==0.3.0", "WSDiscovery==2.0.0"], + "requirements": ["onvif-zeep-async==0.4.0", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 2754a985f36..04e3c9eb497 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ oemthermostat==1.1 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==0.3.0 +onvif-zeep-async==0.4.0 # homeassistant.components.opengarage open-garage==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95aceb77f8a..8e143ff875c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ numpy==1.18.4 oauth2client==4.0.0 # homeassistant.components.onvif -onvif-zeep-async==0.3.0 +onvif-zeep-async==0.4.0 # homeassistant.components.openerz openerz-api==0.1.0 From 940152546458e718136fe085a93107cd8c35fdc5 Mon Sep 17 00:00:00 2001 From: Hugues Granger Date: Fri, 22 May 2020 13:08:53 +0200 Subject: [PATCH 19/68] Fix typo in conf[CONF_SSL] (#35946) --- homeassistant/components/zabbix/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index 644d35da728..a1b4327470f 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -40,7 +40,7 @@ def setup(hass, config): """Set up the Zabbix component.""" conf = config[DOMAIN] - protocol = "https" if config[CONF_SSL] else "http" + protocol = "https" if conf[CONF_SSL] else "http" url = urljoin(f"{protocol}://{conf[CONF_HOST]}", conf[CONF_PATH]) username = conf.get(CONF_USERNAME) From 970bb346d03c9d6f6042cfa958b7b1b4695a1eaf Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 22 May 2020 12:07:01 +0200 Subject: [PATCH 20/68] Fix Daikin AC integration for AirBase units (#35952) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 9732962de5a..9b4e76e5eb1 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.0.2"], + "requirements": ["pydaikin==2.0.4"], "codeowners": ["@fredrike"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 04e3c9eb497..caac67fd1dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1263,7 +1263,7 @@ pycsspeechtts==1.0.3 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.0.2 +pydaikin==2.0.4 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e143ff875c..d19dfcca291 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,7 +533,7 @@ pychromecast==5.1.0 pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==2.0.2 +pydaikin==2.0.4 # homeassistant.components.deconz pydeconz==70 From f4f031e70311ff4f38dcd4d5cabb6b412eb1a61f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 23 May 2020 13:02:49 -0500 Subject: [PATCH 21/68] Improve ipp unique id parsing (#35959) --- homeassistant/components/ipp/config_flow.py | 10 +++++----- tests/components/ipp/test_config_flow.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 3128583f218..ba12d7ec8e2 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -85,12 +85,12 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): unique_id = user_input[CONF_UUID] = info[CONF_UUID] - if unique_id is None and info[CONF_SERIAL] is not None: + if not unique_id and info[CONF_SERIAL]: _LOGGER.debug( "Printer UUID is missing from IPP response. Falling back to IPP serial number" ) unique_id = info[CONF_SERIAL] - elif unique_id is None: + elif not unique_id: _LOGGER.debug("Unable to determine unique id from IPP response") await self.async_set_unique_id(unique_id) @@ -138,17 +138,17 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="ipp_error") unique_id = self.discovery_info[CONF_UUID] - if unique_id is None and info[CONF_UUID] is not None: + if not unique_id and info[CONF_UUID]: _LOGGER.debug( "Printer UUID is missing from discovery info. Falling back to IPP UUID" ) unique_id = self.discovery_info[CONF_UUID] = info[CONF_UUID] - elif unique_id is None and info[CONF_SERIAL] is not None: + elif not unique_id and info[CONF_SERIAL]: _LOGGER.debug( "Printer UUID is missing from discovery info and IPP response. Falling back to IPP serial number" ) unique_id = info[CONF_SERIAL] - elif unique_id is None: + elif not unique_id: _LOGGER.debug( "Unable to determine unique id from discovery info and IPP response" ) diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 0093ba57e5b..a468115f239 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -264,6 +264,24 @@ async def test_zeroconf_with_uuid_device_exists_abort( assert result["reason"] == "already_configured" +async def test_zeroconf_empty_unique_id_required_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if printer lacks (empty) unique identification.""" + mock_connection(aioclient_mock, no_unique_id=True) + + discovery_info = { + **MOCK_ZEROCONF_IPP_SERVICE_INFO, + "properties": {**MOCK_ZEROCONF_IPP_SERVICE_INFO["properties"], "UUID": ""}, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unique_id_required" + + async def test_zeroconf_unique_id_required_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: From 9e1c35cfd78e00a24cd3bce6731f2ca882b079f0 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 22 May 2020 12:13:37 -0400 Subject: [PATCH 22/68] Bump pyAV and close unclosed outputs (#35960) * bump pyAV and close unclosed outputs * skip stream from coverage for now * fix divide by zero error --- .coveragerc | 1 + homeassistant/components/stream/core.py | 5 ++++- homeassistant/components/stream/manifest.json | 2 +- homeassistant/components/stream/worker.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index b468e3ef746..18c4d65c955 100644 --- a/.coveragerc +++ b/.coveragerc @@ -727,6 +727,7 @@ omit = homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* homeassistant/components/stookalert/* + homeassistant/components/stream/* homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 9282c2cb855..153c006ccb2 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -79,8 +79,11 @@ class StreamOutput: @property def target_duration(self) -> int: """Return the average duration of the segments in seconds.""" + segment_length = len(self._segments) + if not segment_length: + return 0 durations = [s.duration for s in self._segments] - return round(sum(durations) // len(self._segments)) or 1 + return round(sum(durations) // segment_length) or 1 def get_segment(self, sequence: int = None) -> Any: """Retrieve a specific segment, or the whole list.""" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 2cc60938a8d..e90d93cbfe3 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["av==7.0.1"], + "requirements": ["av==8.0.1"], "dependencies": ["http"], "codeowners": ["@hunterjm"], "quality_scale": "internal" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 6cd07c7f926..15c1c3c02ff 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -164,3 +164,7 @@ def stream_worker(hass, stream, quit_event): # Assign the video packet to the new stream & mux packet.stream = buffer.vstream buffer.output.mux(packet) + + # Close stream + buffer.output.close() + container.close() diff --git a/requirements_all.txt b/requirements_all.txt index caac67fd1dc..cbc40f10cfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -291,7 +291,7 @@ atenpdu==0.3.0 aurorapy==0.2.6 # homeassistant.components.stream -av==7.0.1 +av==8.0.1 # homeassistant.components.avea avea==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d19dfcca291..8fba5e225db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -138,7 +138,7 @@ arcam-fmj==0.4.4 async-upnp-client==0.14.13 # homeassistant.components.stream -av==7.0.1 +av==8.0.1 # homeassistant.components.axis axis==25 From 39f99819141ee8abde1ab49b190a733fcc72c781 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 22 May 2020 23:01:48 +0200 Subject: [PATCH 23/68] Fix lutron_caseta setup options (#35974) --- .coveragerc | 8 ++- .../components/lutron_caseta/__init__.py | 6 +-- .../components/lutron_caseta/config_flow.py | 5 -- .../components/lutron_caseta/manifest.json | 11 +++-- homeassistant/generated/config_flows.py | 1 - .../lutron_caseta/test_config_flow.py | 49 ++++++++++++++++--- 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/.coveragerc b/.coveragerc index 18c4d65c955..0240e24b74a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -434,7 +434,13 @@ omit = homeassistant/components/luftdaten/* homeassistant/components/lupusec/* homeassistant/components/lutron/* - homeassistant/components/lutron_caseta/* + homeassistant/components/lutron_caseta/__init__.py + homeassistant/components/lutron_caseta/binary_sensor.py + homeassistant/components/lutron_caseta/cover.py + homeassistant/components/lutron_caseta/fan.py + homeassistant/components/lutron_caseta/light.py + homeassistant/components/lutron_caseta/scene.py + homeassistant/components/lutron_caseta/switch.py homeassistant/components/lw12wifi/light.py homeassistant/components/lyft/sensor.py homeassistant/components/magicseaweed/sensor.py diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 59fd81e650e..ff7ec61ecc8 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -39,11 +39,7 @@ LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_ async def async_setup(hass, base_config): """Set up the Lutron component.""" - bridge_configs = base_config.get(DOMAIN) - - if not bridge_configs: - return True - + bridge_configs = base_config[DOMAIN] hass.data.setdefault(DOMAIN, {}) for config in bridge_configs: diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 45a7f10fbf0..3a5a4a151a1 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -94,11 +94,6 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await bridge.close() return True - except (KeyError, ValueError): - _LOGGER.error( - "Error while checking connectivity to bridge %s", self.data[CONF_HOST], - ) - return False except Exception: # pylint: disable=broad-except _LOGGER.exception( "Unknown exception while checking connectivity to bridge %s", diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 7b55dfd9c87..34fc326425c 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,10 @@ "domain": "lutron_caseta", "name": "Lutron Caséta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.6.1"], - "codeowners": ["@swails"], - "config_flow": true -} \ No newline at end of file + "requirements": [ + "pylutron-caseta==0.6.1" + ], + "codeowners": [ + "@swails" + ] +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 154fb024112..14ad0783380 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -83,7 +83,6 @@ FLOWS = [ "locative", "logi_circle", "luftdaten", - "lutron_caseta", "mailgun", "melcloud", "met", diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index a528e223e44..fc9c5fe279d 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Lutron Caseta config flow.""" -from asynctest import patch from pylutron_caseta.smartbridge import Smartbridge from homeassistant import config_entries, data_entry_flow @@ -14,6 +13,7 @@ from homeassistant.components.lutron_caseta.const import ( ) from homeassistant.const import CONF_HOST +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry @@ -51,7 +51,11 @@ async def test_bridge_import_flow(hass): with patch( "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch.object(Smartbridge, "create_tls") as create_tls: + ) as mock_setup_entry, patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ), patch.object( + Smartbridge, "create_tls" + ) as create_tls: create_tls.return_value = MockBridge(can_connect=True) result = await hass.config_entries.flow.async_init( @@ -77,9 +81,7 @@ async def test_bridge_cannot_connect(hass): CONF_CA_CERTS: "", } - with patch( - "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch.object(Smartbridge, "create_tls") as create_tls: + with patch.object(Smartbridge, "create_tls") as create_tls: create_tls.return_value = MockBridge(can_connect=False) result = await hass.config_entries.flow.async_init( @@ -91,8 +93,41 @@ async def test_bridge_cannot_connect(hass): assert result["type"] == "form" assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} - # validate setup_entry was not called - assert len(mock_setup_entry.mock_calls) == 0 + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT + + +async def test_bridge_cannot_connect_unknown_error(hass): + """Test checking for connection and encountering an unknown error.""" + + entry_mock_data = { + CONF_HOST: "", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + } + + with patch.object(Smartbridge, "create_tls") as create_tls: + mock_bridge = MockBridge() + mock_bridge.connect = AsyncMock(side_effect=Exception()) + create_tls.return_value = mock_bridge + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_mock_data, + ) + + assert result["type"] == "form" + assert result["step_id"] == STEP_IMPORT_FAILED + assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT async def test_duplicate_bridge_import(hass): From 5ffee336d51bd3f9b90759043a881267913b32d4 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 23 May 2020 09:46:03 +0200 Subject: [PATCH 24/68] Fix device_registry cleanup behavior (#35977) * Fix: Only decives which are not referenced by an entity or a config_entry are removed * Adapted test for async_cleanup * Changed variable names --- homeassistant/helpers/device_registry.py | 18 +++++++++++++----- tests/helpers/test_device_registry.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 8fbb81962ff..2e91d0d6622 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -399,17 +399,25 @@ def async_cleanup( ent_reg: "entity_registry.EntityRegistry", ) -> None: """Clean up device registry.""" - # Find all devices that are no longer referenced in the entity registry. - referenced = {entry.device_id for entry in ent_reg.entities.values()} - orphan = set(dev_reg.devices) - referenced + # Find all devices that are referenced by a config_entry. + config_entry_ids = {entry.entry_id for entry in hass.config_entries.async_entries()} + references_config_entries = { + device.id + for device in dev_reg.devices.values() + for config_entry_id in device.config_entries + if config_entry_id in config_entry_ids + } + + # Find all devices that are referenced in the entity registry. + references_entities = {entry.device_id for entry in ent_reg.entities.values()} + + orphan = set(dev_reg.devices) - references_entities - references_config_entries for dev_id in orphan: dev_reg.async_remove_device(dev_id) # Find all referenced config entries that no longer exist # This shouldn't happen but have not been able to track down the bug :( - config_entry_ids = {entry.entry_id for entry in hass.config_entries.async_entries()} - for device in list(dev_reg.devices.values()): for config_entry_id in device.config_entries: if config_entry_id not in config_entry_ids: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index ef5f92de79c..3fbb73a2aa8 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -539,7 +539,7 @@ async def test_cleanup_device_registry(hass, registry): device_registry.async_cleanup(hass, registry, ent_reg) assert registry.async_get_device({("hue", "d1")}, set()) is not None - assert registry.async_get_device({("hue", "d2")}, set()) is None + assert registry.async_get_device({("hue", "d2")}, set()) is not None assert registry.async_get_device({("hue", "d3")}, set()) is not None assert registry.async_get_device({("something", "d4")}, set()) is None From 47995edd10481f8a34aad8ec8792c563bce09d37 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 22 May 2020 23:01:05 +0200 Subject: [PATCH 25/68] Bump python-openzwave-mqtt to 1.0.2 (#35980) --- homeassistant/components/ozw/manifest.json | 14 +++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index 3b828845852..c6c96ed15a2 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -3,7 +3,15 @@ "name": "OpenZWave (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", - "requirements": ["python-openzwave-mqtt==1.0.1"], - "after_dependencies": ["mqtt"], - "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"] + "requirements": [ + "python-openzwave-mqtt==1.0.2" + ], + "after_dependencies": [ + "mqtt" + ], + "codeowners": [ + "@cgarwood", + "@marcelveldt", + "@MartinHjelmare" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index cbc40f10cfd..cb0d7a7f845 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1711,7 +1711,7 @@ python-nest==4.1.0 python-nmap==0.6.1 # homeassistant.components.ozw -python-openzwave-mqtt==1.0.1 +python-openzwave-mqtt==1.0.2 # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fba5e225db..28fb458c8d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -705,7 +705,7 @@ python-miio==0.5.0.1 python-nest==4.1.0 # homeassistant.components.ozw -python-openzwave-mqtt==1.0.1 +python-openzwave-mqtt==1.0.2 # homeassistant.components.songpal python-songpal==0.12 From 1feb8ada6c18b4055fc0c854553741b227418bc6 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 23 May 2020 06:10:06 -0300 Subject: [PATCH 26/68] Handle StorageError in the Broadlink integration (#35986) --- homeassistant/components/broadlink/__init__.py | 7 ++++--- homeassistant/components/broadlink/remote.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 040b22945fd..573538f63bb 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging import re -from broadlink.exceptions import BroadlinkException, ReadError +from broadlink.exceptions import BroadlinkException, ReadError, StorageError import voluptuous as vol from homeassistant.const import CONF_HOST @@ -85,10 +85,11 @@ async def async_setup_service(hass, host, device): _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=20): + await asyncio.sleep(1) try: packet = await device.async_request(device.api.check_data) - except ReadError: - await asyncio.sleep(1) + except (ReadError, StorageError): + continue except BroadlinkException as err_msg: _LOGGER.error("Failed to learn: %s", err_msg) return diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index b03bf7a4a04..03ecb9b7634 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -14,6 +14,7 @@ from broadlink.exceptions import ( BroadlinkException, DeviceOfflineError, ReadError, + StorageError, ) import voluptuous as vol @@ -321,10 +322,11 @@ class BroadlinkRemote(RemoteEntity): code = None start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=timeout): + await asyncio.sleep(1) try: code = await self.device.async_request(self.device.api.check_data) - except ReadError: - await asyncio.sleep(1) + except (ReadError, StorageError): + continue else: break From 22a12cea5c28ba4f47ef220daae2ad90608fcb47 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 22 May 2020 22:50:03 +0200 Subject: [PATCH 27/68] Update frontend to 20200519.4 (#35987) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 042ae78535d..89f83d8fa65 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200519.3"], + "requirements": ["home-assistant-frontend==20200519.4"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b6f19f3e728..909c42cf9ef 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 hass-nabucasa==0.34.2 -home-assistant-frontend==20200519.3 +home-assistant-frontend==20200519.4 importlib-metadata==1.6.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index cb0d7a7f845..b8994de51eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.3 +home-assistant-frontend==20200519.4 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28fb458c8d4..8a280579b99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.3 +home-assistant-frontend==20200519.4 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From cf6cc6c07cf389032bd47d3af226c5488a2a969a Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 22 May 2020 21:11:30 -0400 Subject: [PATCH 28/68] Fix ONVIF PTZ and profile encoding issues (#36006) * allow lib to create AsyncTransport * fix transport close issue * fix zoom only cameras without PTZ presets * catch profiles without encoding configuration * also catch ServerDisconnectedError for ptz --- homeassistant/components/onvif/config_flow.py | 3 ++- homeassistant/components/onvif/device.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index ceb861fc7dd..1dba697380d 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -219,7 +219,8 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): media_service = device.create_media_service() profiles = await media_service.GetProfiles() h264 = any( - profile.VideoEncoderConfiguration.Encoding == "H264" + profile.VideoEncoderConfiguration + and profile.VideoEncoderConfiguration.Encoding == "H264" for profile in profiles ) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index d6f407f016d..938c960080f 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -255,7 +255,10 @@ class ONVIFDevice: profiles = [] for key, onvif_profile in enumerate(result): # Only add H264 profiles - if onvif_profile.VideoEncoderConfiguration.Encoding != "H264": + if ( + not onvif_profile.VideoEncoderConfiguration + or onvif_profile.VideoEncoderConfiguration.Encoding != "H264" + ): continue profile = Profile( @@ -282,9 +285,13 @@ class ONVIFDevice: is not None, ) - ptz_service = self.device.create_ptz_service() - presets = await ptz_service.GetPresets(profile.token) - profile.ptz.presets = [preset.token for preset in presets] + try: + ptz_service = self.device.create_ptz_service() + presets = await ptz_service.GetPresets(profile.token) + profile.ptz.presets = [preset.token for preset in presets] + except (Fault, ServerDisconnectedError): + # It's OK if Presets aren't supported + profile.ptz.presets = [] profiles.append(profile) From 375f170da8c5ecf5351330bfe64989b51ec84031 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 May 2020 19:57:39 +0200 Subject: [PATCH 29/68] Upgrade hass-nabucasa to 0.34.3 (#36025) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index de5496cfd99..fcd6738b77c 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.34.2"], + "requirements": ["hass-nabucasa==0.34.3"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 909c42cf9ef..e8c3550e996 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ ciso8601==2.1.3 cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 -hass-nabucasa==0.34.2 +hass-nabucasa==0.34.3 home-assistant-frontend==20200519.4 importlib-metadata==1.6.0 jinja2>=2.11.1 diff --git a/requirements_all.txt b/requirements_all.txt index b8994de51eb..0ced214832d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -701,7 +701,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.34.2 +hass-nabucasa==0.34.3 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a280579b99..32c44bae8e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -294,7 +294,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.34.2 +hass-nabucasa==0.34.3 # homeassistant.components.mqtt hbmqtt==0.9.5 From 88ebc6a08bbbdb0d971540075be88f708c60fa6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Oldag?= Date: Sat, 23 May 2020 14:09:59 +0200 Subject: [PATCH 30/68] Migrate rpi_gpio_pwm to extend LightEntity instead of Light (#36028) --- homeassistant/components/rpi_gpio_pwm/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py index 96ac3c6f2ed..f86d1f27832 100644 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ b/homeassistant/components/rpi_gpio_pwm/light.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv @@ -104,7 +104,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(leds) -class PwmSimpleLed(Light, RestoreEntity): +class PwmSimpleLed(LightEntity, RestoreEntity): """Representation of a simple one-color PWM LED.""" def __init__(self, led, name): From bd32a1fabbc106d7ec25e138b49b4e18f7eed3fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 May 2020 12:56:27 -0500 Subject: [PATCH 31/68] Fix shade compatibility with hunter douglas powerview 1.0 hubs (#36040) --- .../hunterdouglas_powerview/cover.py | 6 +++++ .../hunterdouglas_powerview/entity.py | 22 ++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index e14142677e3..8135b4a8c77 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -68,6 +68,12 @@ async def async_setup_entry(hass, entry, async_add_entities): except asyncio.TimeoutError: # Forced refresh is not required for setup pass + if ATTR_POSITION_DATA not in shade.raw_data: + _LOGGER.info( + "The %s shade was skipped because it is missing position data", + name_before_refresh, + ) + continue entities.append( PowerViewShade( shade, name_before_refresh, room_data, coordinator, device_info diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 3c98eeaf615..f89ca28023b 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -74,6 +74,17 @@ class ShadeEntity(HDEntity): @property def device_info(self): """Return the device_info of the device.""" + + device_info = { + "identifiers": {(DOMAIN, self._shade.id)}, + "name": self._shade_name, + "manufacturer": MANUFACTURER, + "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), + } + + if FIRMWARE_IN_SHADE not in self._shade.raw_data: + return device_info + firmware = self._shade.raw_data[FIRMWARE_IN_SHADE] sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" model = self._shade.raw_data[ATTR_TYPE] @@ -82,11 +93,6 @@ class ShadeEntity(HDEntity): model = shade.description break - return { - "identifiers": {(DOMAIN, self._shade.id)}, - "name": self._shade_name, - "model": str(model), - "sw_version": sw_version, - "manufacturer": MANUFACTURER, - "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), - } + device_info["sw_version"] = sw_version + device_info["model"] = model + return device_info From a90d1b531ccc3658e1b7a4126bdcd37d24ab4654 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 23 May 2020 18:59:32 +0100 Subject: [PATCH 32/68] Bump aiohomekit (#36041) --- .../components/homekit_controller/connection.py | 10 ++-------- .../components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 605253e6235..d910de34321 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -89,10 +89,6 @@ class HKDevice: # mapped to a HA entity. self.entities = [] - # There are multiple entities sharing a single connection - only - # allow one entity to use pairing at once. - self.pairing_lock = asyncio.Lock() - self.available = True self.signal_state_updated = "_".join((DOMAIN, self.unique_id, "state_updated")) @@ -333,13 +329,11 @@ class HKDevice: async def get_characteristics(self, *args, **kwargs): """Read latest state from homekit accessory.""" - async with self.pairing_lock: - return await self.pairing.get_characteristics(*args, **kwargs) + return await self.pairing.get_characteristics(*args, **kwargs) async def put_characteristics(self, characteristics): """Control a HomeKit device state from Home Assistant.""" - async with self.pairing_lock: - results = await self.pairing.put_characteristics(characteristics) + results = await self.pairing.put_characteristics(characteristics) # Feed characteristics back into HA and update the current state # results will only contain failures, so anythin in characteristics diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 07736f61c8e..961dd380ac1 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.37"], + "requirements": ["aiohomekit[IP]==0.2.38"], "zeroconf": ["_hap._tcp.local."], "codeowners": ["@Jc2k"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0ced214832d..aa92eb20210 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aioftp==0.12.0 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.37 +aiohomekit[IP]==0.2.38 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32c44bae8e7..198251cac6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -82,7 +82,7 @@ aiofreepybox==0.0.8 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.37 +aiohomekit[IP]==0.2.38 # homeassistant.components.emulated_hue # homeassistant.components.http From ed4b1d7c9fb3115d7d845f53974801225d4b7c80 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sat, 23 May 2020 21:47:24 +0200 Subject: [PATCH 33/68] Bump tellduslive version (#36048) --- homeassistant/components/tellduslive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 55149369427..7ad65b4abd4 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -3,7 +3,7 @@ "name": "Telldus Live", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tellduslive", - "requirements": ["tellduslive==0.10.10"], + "requirements": ["tellduslive==0.10.11"], "codeowners": ["@fredrike"], "quality_scale": "gold" } diff --git a/requirements_all.txt b/requirements_all.txt index aa92eb20210..3a7bc962114 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2063,7 +2063,7 @@ tellcore-net==0.4 tellcore-py==1.1.2 # homeassistant.components.tellduslive -tellduslive==0.10.10 +tellduslive==0.10.11 # homeassistant.components.lg_soundbar temescal==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 198251cac6d..59a9b17b4a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -826,7 +826,7 @@ stringcase==1.2.0 sunwatcher==0.2.1 # homeassistant.components.tellduslive -tellduslive==0.10.10 +tellduslive==0.10.11 # homeassistant.components.powerwall tesla-powerwall==0.2.8 From fb3394f74bc4cb2a9827131bb1184bad5ef4d7a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 May 2020 11:30:01 +0200 Subject: [PATCH 34/68] Bumped version to 0.110.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 366c2c79d99..8993225f283 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 110 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From ab741535f7c0d1627fda950632773c929306af7a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 23 May 2020 04:06:48 -0500 Subject: [PATCH 35/68] Fix roku play/pause support (#35991) --- homeassistant/components/roku/media_player.py | 10 ++++++++ tests/components/roku/test_media_player.py | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 7b64888bbd1..8c92eff3687 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -7,6 +7,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, @@ -29,6 +30,7 @@ SUPPORT_ROKU = ( | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + | SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_ON @@ -167,6 +169,14 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Turn off the Roku.""" await self.coordinator.roku.remote("poweroff") + async def async_media_pause(self) -> None: + """Send pause command.""" + await self.coordinator.roku.remote("play") + + async def async_media_play(self) -> None: + """Send play command.""" + await self.coordinator.roku.remote("play") + async def async_media_play_pause(self) -> None: """Send play/pause command.""" await self.coordinator.roku.remote("play") diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index f91a8b286b3..9d809cae433 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, @@ -30,6 +31,8 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_TURN_OFF, @@ -142,6 +145,7 @@ async def test_supported_features( | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + | SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_ON @@ -170,6 +174,7 @@ async def test_tv_supported_features( | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + | SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_ON @@ -267,6 +272,26 @@ async def test_services( remote_mock.assert_called_once_with("poweron") + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_MEDIA_PAUSE, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, + blocking=True, + ) + + remote_mock.assert_called_once_with("play") + + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_MEDIA_PLAY, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, + blocking=True, + ) + + remote_mock.assert_called_once_with("play") + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, From 011e0a0fccf2e7e9701baeeabe2867c127a167fc Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Mon, 25 May 2020 08:31:49 +0200 Subject: [PATCH 36/68] Bump pyvlx to 0.2.16 (#35971) --- homeassistant/components/velux/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index e6747060da6..73306bca7b5 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,6 +2,6 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.14"], + "requirements": ["pyvlx==0.2.16"], "codeowners": ["@Julius2342"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a7bc962114..44683e4ff38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1802,7 +1802,7 @@ pyvesync==1.1.0 pyvizio==0.1.47 # homeassistant.components.velux -pyvlx==0.2.14 +pyvlx==0.2.16 # homeassistant.components.html5 pywebpush==1.9.2 From 1eeba0531e953d1518ac58d435787a5c8233203b Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Fri, 22 May 2020 23:55:53 -0700 Subject: [PATCH 37/68] Bump iaqualink to 0.3.3 (#35999) --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index d9d16038d19..68b4554f73a 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.1"] + "requirements": ["iaqualink==0.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 44683e4ff38..1a96e4ffb29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,7 +764,7 @@ hydrawiser==0.1.1 iammeter==0.1.7 # homeassistant.components.iaqualink -iaqualink==0.3.1 +iaqualink==0.3.3 # homeassistant.components.watson_tts ibm-watson==4.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59a9b17b4a2..417937253ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,7 +331,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.12 # homeassistant.components.iaqualink -iaqualink==0.3.1 +iaqualink==0.3.3 # homeassistant.components.influxdb influxdb==5.2.3 From fad79046a876f11a2eefc90138063ebd6c26a811 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Fri, 22 May 2020 23:53:10 -0700 Subject: [PATCH 38/68] Fix iaqualink sensors (#36000) * iaqualink: small sensor fixes * Re-add device_class, fix type hints. --- homeassistant/components/iaqualink/sensor.py | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 81021d0b447..80f18dc8191 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -34,16 +34,25 @@ class HassAqualinkSensor(AqualinkEntity): return self.dev.label @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> Optional[str]: """Return the measurement unit for the sensor.""" - if self.dev.system.temp_unit == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS + if self.dev.name.endswith("_temp"): + if self.dev.system.temp_unit == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + return None @property - def state(self) -> str: + def state(self) -> Optional[str]: """Return the state of the sensor.""" - return int(self.dev.state) if self.dev.state != "" else None + if self.dev.state == "": + return None + + try: + state = int(self.dev.state) + except ValueError: + state = float(self.dev.state) + return state @property def device_class(self) -> Optional[str]: From 179e601966e864b98ad7c8108c7d43732d097eb5 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sun, 24 May 2020 15:50:50 -0400 Subject: [PATCH 39/68] Fix ONVIF config entry unique ID (#36008) * fallback to device serial number if no mac available * make password optional to fix #35904 * update tests to reflect new flow * fix snake case and AsyncMock * add comments around why weird things are being done --- homeassistant/components/onvif/base.py | 24 +++++-- homeassistant/components/onvif/camera.py | 4 +- homeassistant/components/onvif/config_flow.py | 20 ++++-- homeassistant/components/onvif/device.py | 19 +++-- homeassistant/components/onvif/event.py | 5 +- homeassistant/components/onvif/models.py | 1 + tests/components/onvif/test_config_flow.py | 70 +++++++++++++++---- 7 files changed, 112 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 43a846cac37..cca84c4562a 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -2,6 +2,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import Entity +from .const import DOMAIN from .device import ONVIFDevice from .models import Profile @@ -11,8 +12,8 @@ class ONVIFBaseEntity(Entity): def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None: """Initialize the ONVIF entity.""" - self.device = device - self.profile = profile + self.device: ONVIFDevice = device + self.profile: Profile = profile @property def available(self): @@ -22,10 +23,25 @@ class ONVIFBaseEntity(Entity): @property def device_info(self): """Return a device description for device registry.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self.device.info.mac)}, + device_info = { "manufacturer": self.device.info.manufacturer, "model": self.device.info.model, "name": self.device.name, "sw_version": self.device.info.fw_version, } + + # MAC address is not always available, and given the number + # of non-conformant ONVIF devices we have historically supported, + # we can not guarantee serial number either. Due to this, we have + # adopted an either/or approach in the config entry setup, and can + # guarantee that one or the other will be populated. + # See: https://github.com/home-assistant/core/issues/35883 + if self.device.info.serial_number: + device_info["identifiers"] = {(DOMAIN, self.device.info.serial_number)} + + if self.device.info.mac: + device_info["connections"] = { + (CONNECTION_NETWORK_MAC, self.device.info.mac) + } + + return device_info diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 570b99bfe3a..7f97e0fbea4 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -96,8 +96,8 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): def unique_id(self) -> str: """Return a unique ID.""" if self.profile.index: - return f"{self.device.info.mac}_{self.profile.index}" - return self.device.info.mac + return f"{self.device.info.mac or self.device.info.serial_number}_{self.profile.index}" + return self.device.info.mac or self.device.info.serial_number @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 1dba697380d..29784b25429 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -169,10 +169,16 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD] return await self.async_step_profiles() + # Password is optional and default empty due to some cameras not + # allowing you to change ONVIF user settings. + # See https://github.com/home-assistant/core/issues/35904 return self.async_show_form( step_id="auth", data_schema=vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD, default=""): str, + } ), ) @@ -195,15 +201,21 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await device.update_xaddrs() try: + device_mgmt = device.create_devicemgmt_service() + # Get the MAC address to use as the unique ID for the config flow if not self.device_id: - devicemgmt = device.create_devicemgmt_service() - network_interfaces = await devicemgmt.GetNetworkInterfaces() + network_interfaces = await device_mgmt.GetNetworkInterfaces() for interface in network_interfaces: if interface.Enabled: self.device_id = interface.Info.HwAddress - if self.device_id is None: + # If no network interfaces are exposed, fallback to serial number + if not self.device_id: + device_info = await device_mgmt.GetDeviceInformation() + self.device_id = device_info.SerialNumber + + if not self.device_id: return self.async_abort(reason="no_mac") await self.async_set_unique_id(self.device_id, raise_on_progress=False) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 938c960080f..a28e9c4d6c2 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -148,12 +148,12 @@ class ONVIFDevice: async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" LOGGER.debug("Setting up the ONVIF device management service") - devicemgmt = self.device.create_devicemgmt_service() + device_mgmt = self.device.create_devicemgmt_service() LOGGER.debug("Retrieving current device date/time") try: system_date = dt_util.utcnow() - device_time = await devicemgmt.GetSystemDateAndTime() + device_time = await device_mgmt.GetSystemDateAndTime() if not device_time: LOGGER.debug( """Couldn't get device '%s' date/time. @@ -212,13 +212,22 @@ class ONVIFDevice: async def async_get_device_info(self) -> DeviceInfo: """Obtain information about this device.""" - devicemgmt = self.device.create_devicemgmt_service() - device_info = await devicemgmt.GetDeviceInformation() + device_mgmt = self.device.create_devicemgmt_service() + device_info = await device_mgmt.GetDeviceInformation() + + # Grab the last MAC address for backwards compatibility + mac = None + network_interfaces = await device_mgmt.GetNetworkInterfaces() + for interface in network_interfaces: + if interface.Enabled: + mac = interface.Info.HwAddress + return DeviceInfo( device_info.Manufacturer, device_info.Model, device_info.FirmwareVersion, - self.config_entry.unique_id, + device_info.SerialNumber, + mac, ) async def async_get_capabilities(self): diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 183ad0ab532..7a0113177c4 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -62,7 +62,8 @@ class EventManager: @callback def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: """Remove data update.""" - self._listeners.remove(update_callback) + if update_callback in self._listeners: + self._listeners.remove(update_callback) if not self._listeners and self._unsub_refresh: self._unsub_refresh() @@ -93,6 +94,8 @@ class EventManager: async def async_stop(self) -> None: """Unsubscribe from events.""" + self._listeners = [] + if not self._subscription: return diff --git a/homeassistant/components/onvif/models.py b/homeassistant/components/onvif/models.py index 686d9fecbda..2a129d3bc44 100644 --- a/homeassistant/components/onvif/models.py +++ b/homeassistant/components/onvif/models.py @@ -10,6 +10,7 @@ class DeviceInfo: manufacturer: str = None model: str = None fw_version: str = None + serial_number: str = None mac: str = None diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index c709c5e6f67..5c41349bbbe 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -1,13 +1,11 @@ """Test ONVIF config flow.""" -from asyncio import Future - -from asynctest import MagicMock, patch from onvif.exceptions import ONVIFError from zeep.exceptions import Fault from homeassistant import config_entries, data_entry_flow from homeassistant.components.onvif import config_flow +from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry URN = "urn:uuid:123456789" @@ -17,6 +15,7 @@ PORT = 80 USERNAME = "admin" PASSWORD = "12345" MAC = "aa:bb:cc:dd:ee" +SERIAL_NUMBER = "ABCDEFGHIJK" DISCOVERY = [ { @@ -37,18 +36,25 @@ DISCOVERY = [ def setup_mock_onvif_camera( - mock_onvif_camera, with_h264=True, two_profiles=False, with_interfaces=True + mock_onvif_camera, + with_h264=True, + two_profiles=False, + with_interfaces=True, + with_serial=True, ): """Prepare mock onvif.ONVIFCamera.""" devicemgmt = MagicMock() + device_info = MagicMock() + device_info.SerialNumber = SERIAL_NUMBER if with_serial else None + devicemgmt.GetDeviceInformation = AsyncMock(return_value=device_info) + interface = MagicMock() interface.Enabled = True interface.Info.HwAddress = MAC - devicemgmt.GetNetworkInterfaces.return_value = Future() - devicemgmt.GetNetworkInterfaces.return_value.set_result( - [interface] if with_interfaces else [] + devicemgmt.GetNetworkInterfaces = AsyncMock( + return_value=[interface] if with_interfaces else [] ) media_service = MagicMock() @@ -58,11 +64,9 @@ def setup_mock_onvif_camera( profile2 = MagicMock() profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG" - media_service.GetProfiles.return_value = Future() - media_service.GetProfiles.return_value.set_result([profile1, profile2]) + media_service.GetProfiles = AsyncMock(return_value=[profile1, profile2]) - mock_onvif_camera.update_xaddrs.return_value = Future() - mock_onvif_camera.update_xaddrs.return_value.set_result(True) + mock_onvif_camera.update_xaddrs = AsyncMock(return_value=True) mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt) mock_onvif_camera.create_media_service = MagicMock(return_value=media_service) @@ -116,8 +120,7 @@ def setup_mock_discovery( def setup_mock_device(mock_device): """Prepare mock ONVIFDevice.""" - mock_device.async_setup.return_value = Future() - mock_device.async_setup.return_value.set_result(True) + mock_device.async_setup = AsyncMock(return_value=True) def mock_constructor(hass, config): """Fake the controller constructor.""" @@ -390,11 +393,48 @@ async def test_flow_manual_entry(hass): async def test_flow_import_no_mac(hass): - """Test that config flow fails when no MAC available.""" + """Test that config flow uses Serial Number when no MAC available.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera, with_interfaces=False) + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{NAME} - {SERIAL_NUMBER}" + assert result["data"] == { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + } + + +async def test_flow_import_no_mac_or_serial(hass): + """Test that config flow fails when no MAC or Serial Number available.""" with patch( "homeassistant.components.onvif.config_flow.get_device" ) as mock_onvif_camera: - setup_mock_onvif_camera(mock_onvif_camera, with_interfaces=False) + setup_mock_onvif_camera( + mock_onvif_camera, with_interfaces=False, with_serial=False + ) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, From b6c519e5dfb08e3fccb54e8d0d86eae1d7b02891 Mon Sep 17 00:00:00 2001 From: isk0001y Date: Mon, 25 May 2020 14:29:26 +0200 Subject: [PATCH 40/68] Re-read last imap_email_content email when no change (#36065) --- homeassistant/components/imap_email_content/sensor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index ea93c2bb975..21b535450a1 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -103,6 +103,8 @@ class EmailReader: if message_data is None: return None + if message_data[0] is None: + return None raw_email = message_data[0][1] email_message = email.message_from_bytes(raw_email) return email_message @@ -126,13 +128,22 @@ class EmailReader: self._last_id = int(message_uid) return self._fetch_message(message_uid) + return self._fetch_message(str(self._last_id)) + except imaplib.IMAP4.error: _LOGGER.info("Connection to %s lost, attempting to reconnect", self._server) try: self.connect() + _LOGGER.info( + "Reconnect to %s succeeded, trying last message", self._server + ) + if self._last_id is not None: + return self._fetch_message(str(self._last_id)) except imaplib.IMAP4.error: _LOGGER.error("Failed to reconnect") + return None + class EmailContentSensor(Entity): """Representation of an EMail sensor.""" From 7e90d4dd7b0d001a85e8a7b962ae409cc598de61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 24 May 2020 14:28:54 +0200 Subject: [PATCH 41/68] Fix opengarage async_setup_platform (#36075) * opengarage async_setup_platform * async_add_entities --- homeassistant/components/opengarage/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 239697a229c..70b4d8c98ee 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -55,7 +55,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the OpenGarage covers.""" covers = [] devices = config.get(CONF_COVERS) @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): covers.append(OpenGarageCover(device_config.get(CONF_NAME), open_garage)) - add_entities(covers, True) + async_add_entities(covers, True) class OpenGarageCover(CoverEntity): From 0f7ea290ca392bac4a180bda09deeb14238cd272 Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Mon, 25 May 2020 21:55:23 +0200 Subject: [PATCH 42/68] Fix emulated_hue compatibility with older devices (#36090) * Fix emulated_hue compatibility with older devices * Fix test ugliness * Fix pylint errors --- .../components/emulated_hue/const.py | 4 ++ homeassistant/components/emulated_hue/upnp.py | 55 +++++++++++------ tests/components/emulated_hue/test_upnp.py | 60 +++++++++++++++++++ 3 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/emulated_hue/const.py diff --git a/homeassistant/components/emulated_hue/const.py b/homeassistant/components/emulated_hue/const.py new file mode 100644 index 00000000000..bfd58c5a0e1 --- /dev/null +++ b/homeassistant/components/emulated_hue/const.py @@ -0,0 +1,4 @@ +"""Constants for emulated_hue.""" + +HUE_SERIAL_NUMBER = "001788FFFE23BFC2" +HUE_UUID = "2f402f80-da50-11e1-9b23-001788255acc" diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index f0fe392f865..14e3cf11ca2 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -9,6 +9,8 @@ from aiohttp import web from homeassistant import core from homeassistant.components.http import HomeAssistantView +from .const import HUE_SERIAL_NUMBER, HUE_UUID + _LOGGER = logging.getLogger(__name__) @@ -42,8 +44,8 @@ class DescriptionXmlView(HomeAssistantView): Philips hue bridge 2015 BSB002 http://www.meethue.com -001788FFFE23BFC2 -uuid:2f402f80-da50-11e1-9b23-001788255acc +{HUE_SERIAL_NUMBER} +uuid:{HUE_UUID} """ @@ -70,21 +72,8 @@ class UPNPResponderThread(threading.Thread): self.host_ip_addr = host_ip_addr self.listen_port = listen_port self.upnp_bind_multicast = upnp_bind_multicast - - # Note that the double newline at the end of - # this string is required per the SSDP spec - resp_template = f"""HTTP/1.1 200 OK -CACHE-CONTROL: max-age=60 -EXT: -LOCATION: http://{advertise_ip}:{advertise_port}/description.xml -SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 -hue-bridgeid: 001788FFFE23BFC2 -ST: upnp:rootdevice -USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice - -""" - - self.upnp_response = resp_template.replace("\n", "\r\n").encode("utf-8") + self.advertise_ip = advertise_ip + self.advertise_port = advertise_port def run(self): """Run the server.""" @@ -136,10 +125,13 @@ USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice continue if "M-SEARCH" in data.decode("utf-8", errors="ignore"): + _LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data) # SSDP M-SEARCH method received, respond to it with our info - resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + response = self._handle_request(data) - resp_socket.sendto(self.upnp_response, addr) + resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + resp_socket.sendto(response, addr) + _LOGGER.debug("UPNP Responder responding with: %s", response) resp_socket.close() def stop(self): @@ -148,6 +140,31 @@ USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice self._interrupted = True self.join() + def _handle_request(self, data): + if "upnp:rootdevice" in data.decode("utf-8", errors="ignore"): + return self._prepare_response( + "upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice" + ) + + return self._prepare_response( + "urn:schemas-upnp-org:device:basic:1", f"uuid:{HUE_UUID}" + ) + + def _prepare_response(self, search_target, unique_service_name): + # Note that the double newline at the end of + # this string is required per the SSDP spec + response = f"""HTTP/1.1 200 OK +CACHE-CONTROL: max-age=60 +EXT: +LOCATION: http://{self.advertise_ip}:{self.advertise_port}/description.xml +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: {HUE_SERIAL_NUMBER} +ST: {search_target} +USN: {unique_service_name} + +""" + return response.replace("\n", "\r\n").encode("utf-8") + def clean_socket_close(sock): """Close a socket connection and logs its closure.""" diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 32859ca00c1..a6040e8db68 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -48,6 +48,66 @@ class TestEmulatedHue(unittest.TestCase): """Stop the class.""" cls.hass.stop() + def test_upnp_discovery_basic(self): + """Tests the UPnP basic discovery response.""" + with patch("threading.Thread.__init__"): + upnp_responder_thread = emulated_hue.UPNPResponderThread( + "0.0.0.0", 80, True, "192.0.2.42", 8080 + ) + + """Original request emitted by the Hue Bridge v1 app.""" + request = """M-SEARCH * HTTP/1.1 +HOST:239.255.255.250:1900 +ST:ssdp:all +Man:"ssdp:discover" +MX:3 + +""" + encoded_request = request.replace("\n", "\r\n").encode("utf-8") + + response = upnp_responder_thread._handle_request(encoded_request) + expected_response = """HTTP/1.1 200 OK +CACHE-CONTROL: max-age=60 +EXT: +LOCATION: http://192.0.2.42:8080/description.xml +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: 001788FFFE23BFC2 +ST: urn:schemas-upnp-org:device:basic:1 +USN: uuid:2f402f80-da50-11e1-9b23-001788255acc + +""" + assert expected_response.replace("\n", "\r\n").encode("utf-8") == response + + def test_upnp_discovery_rootdevice(self): + """Tests the UPnP rootdevice discovery response.""" + with patch("threading.Thread.__init__"): + upnp_responder_thread = emulated_hue.UPNPResponderThread( + "0.0.0.0", 80, True, "192.0.2.42", 8080 + ) + + """Original request emitted by Busch-Jaeger free@home SysAP.""" + request = """M-SEARCH * HTTP/1.1 +HOST: 239.255.255.250:1900 +MAN: "ssdp:discover" +MX: 40 +ST: upnp:rootdevice + +""" + encoded_request = request.replace("\n", "\r\n").encode("utf-8") + + response = upnp_responder_thread._handle_request(encoded_request) + expected_response = """HTTP/1.1 200 OK +CACHE-CONTROL: max-age=60 +EXT: +LOCATION: http://192.0.2.42:8080/description.xml +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: 001788FFFE23BFC2 +ST: upnp:rootdevice +USN: uuid:2f402f80-da50-11e1-9b23-001788255acc::upnp:rootdevice + +""" + assert expected_response.replace("\n", "\r\n").encode("utf-8") == response + def test_description_xml(self): """Test the description.""" result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) From 249c00d08fe163331b78b76e214fae4f0354861e Mon Sep 17 00:00:00 2001 From: Minims Date: Mon, 25 May 2020 14:02:21 +0200 Subject: [PATCH 43/68] Fix onvif snapshot for Sricam SP009 (#36095) --- homeassistant/components/onvif/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index a28e9c4d6c2..c4d3146002e 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -237,7 +237,7 @@ class ONVIFDevice: media_service = self.device.create_media_service() media_capabilities = await media_service.GetServiceCapabilities() snapshot = media_capabilities and media_capabilities.SnapshotUri - except (ONVIFError, Fault): + except (ONVIFError, Fault, ServerDisconnectedError): pass pullpoint = False From 3d921cec04c389bf23a7dfae3097b5207fa14cae Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 25 May 2020 06:55:25 -0500 Subject: [PATCH 44/68] Fix roku play/pause during standby (#36096) --- homeassistant/components/roku/media_player.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 8c92eff3687..0deeb44dbc2 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -171,15 +171,18 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): async def async_media_pause(self) -> None: """Send pause command.""" - await self.coordinator.roku.remote("play") + if self.state != STATE_STANDBY: + await self.coordinator.roku.remote("play") async def async_media_play(self) -> None: """Send play command.""" - await self.coordinator.roku.remote("play") + if self.state != STATE_STANDBY: + await self.coordinator.roku.remote("play") async def async_media_play_pause(self) -> None: """Send play/pause command.""" - await self.coordinator.roku.remote("play") + if self.state != STATE_STANDBY: + await self.coordinator.roku.remote("play") async def async_media_previous_track(self) -> None: """Send previous track command.""" From 53c2227b1d3367294e5e320ead584522b8051520 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 26 May 2020 01:00:05 -0600 Subject: [PATCH 45/68] Fix bugs with AirVisual auto-leveling API (#36097) * Fix bugs with AirVisual auto-leveling API * Code review * Code review --- .../components/airvisual/__init__.py | 73 ++++++++++++------- .../components/airvisual/air_quality.py | 8 +- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index e5d8b03f316..96b3770974a 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -38,7 +38,7 @@ from .const import ( PLATFORMS = ["air_quality", "sensor"] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" -DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True} GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema( @@ -95,27 +95,43 @@ def async_get_cloud_api_update_interval(hass, api_key): This will shift based on the number of active consumers, thus keeping the user under the monthly API limit. """ - num_consumers = len( - { - config_entry - for config_entry in hass.config_entries.async_entries(DOMAIN) - if config_entry.data.get(CONF_API_KEY) == api_key - } - ) + num_consumers = len(async_get_cloud_coordinators_by_api_key(hass, api_key)) # Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note # that we give a buffer of 1500 API calls for any drift, restarts, etc.: minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers)) + + LOGGER.debug( + "Leveling API key usage (%s): %s consumers, %s minutes between updates", + api_key, + num_consumers, + minutes_between_api_calls, + ) + return timedelta(minutes=minutes_between_api_calls) @callback -def async_reset_coordinator_update_intervals(hass, update_interval): - """Update any existing data coordinators with a new update interval.""" - if not hass.data[DOMAIN][DATA_COORDINATOR]: - return +def async_get_cloud_coordinators_by_api_key(hass, api_key): + """Get all DataUpdateCoordinator objects related to a particular API key.""" + coordinators = [] + for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): + config_entry = hass.config_entries.async_get_entry(entry_id) + if config_entry.data.get(CONF_API_KEY) == api_key: + coordinators.append(coordinator) + return coordinators - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR].values(): + +@callback +def async_sync_geo_coordinator_update_intervals(hass, api_key): + """Sync the update interval for geography-based data coordinators (by API key).""" + update_interval = async_get_cloud_api_update_interval(hass, api_key) + for coordinator in async_get_cloud_coordinators_by_api_key(hass, api_key): + LOGGER.debug( + "Updating interval for coordinator: %s, %s", + coordinator.name, + update_interval, + ) coordinator.update_interval = update_interval @@ -194,10 +210,6 @@ async def async_setup_entry(hass, config_entry): client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession) - update_interval = async_get_cloud_api_update_interval( - hass, config_entry.data[CONF_API_KEY] - ) - async def async_update_data(): """Get new data from the API.""" if CONF_CITY in config_entry.data: @@ -219,14 +231,19 @@ async def async_setup_entry(hass, config_entry): coordinator = DataUpdateCoordinator( hass, LOGGER, - name="geography data", - update_interval=update_interval, + name=async_get_geography_id(config_entry.data), + # We give a placeholder update interval in order to create the coordinator; + # then, below, we use the coordinator's presence (along with any other + # coordinators using the same API key) to calculate an actual, leveled + # update interval: + update_interval=timedelta(minutes=5), update_method=async_update_data, ) - # Ensure any other, existing config entries that use this API key are updated - # with the new scan interval: - async_reset_coordinator_update_intervals(hass, update_interval) + hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + async_sync_geo_coordinator_update_intervals( + hass, config_entry.data[CONF_API_KEY] + ) # Only geography-based entries have options: config_entry.add_update_listener(async_update_options) @@ -251,13 +268,13 @@ async def async_setup_entry(hass, config_entry): hass, LOGGER, name="Node/Pro data", - update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL, + update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL, update_method=async_update_data, ) - await coordinator.async_refresh() + hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + await coordinator.async_refresh() for component in PLATFORMS: hass.async_create_task( @@ -317,6 +334,12 @@ async def async_unload_entry(hass, config_entry): ) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: + # Re-calculate the update interval period for any remaining consumes of this + # API key: + async_sync_geo_coordinator_update_intervals( + hass, config_entry.data[CONF_API_KEY] + ) return unload_ok diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py index bd1c10a9d84..bb2d64a23db 100644 --- a/homeassistant/components/airvisual/air_quality.py +++ b/homeassistant/components/airvisual/air_quality.py @@ -8,7 +8,7 @@ from .const import ( CONF_INTEGRATION_TYPE, DATA_COORDINATOR, DOMAIN, - INTEGRATION_TYPE_GEOGRAPHY, + INTEGRATION_TYPE_NODE_PRO, ) ATTR_HUMIDITY = "humidity" @@ -18,12 +18,12 @@ ATTR_VOC = "voc" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up AirVisual air quality entities based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] - # Geography-based AirVisual integrations don't utilize this platform: - if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: + if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO: return + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + async_add_entities([AirVisualNodeProSensor(coordinator)], True) From a656a40fd77f6dc0c09bc7477c2a3d5fa82fbd45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 May 2020 11:17:30 -0500 Subject: [PATCH 46/68] Ensure homekit bridge state is restored before creating devices (#36098) * Ensure homekit bridge state is restored before creating devices * Tests to ensure homekit device registry entry is stable * remove stray continue --- homeassistant/components/homekit/__init__.py | 44 ++++++++++++++++++-- tests/components/homekit/test_homekit.py | 41 +++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index adbf79128e3..428f8e30abf 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -2,6 +2,7 @@ import asyncio import ipaddress import logging +import os from aiohttp import web import voluptuous as vol @@ -49,6 +50,7 @@ from .const import ( ATTR_SOFTWARE_VERSION, ATTR_VALUE, BRIDGE_NAME, + BRIDGE_SERIAL_NUMBER, CONF_ADVERTISE_IP, CONF_AUTO_START, CONF_ENTITY_CONFIG, @@ -434,6 +436,13 @@ class HomeKit: interface_choice=self._interface_choice, ) + # If we do not load the mac address will be wrong + # as pyhap uses a random one until state is restored + if os.path.exists(persist_file): + self.driver.load() + else: + self.driver.persist() + self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: _LOGGER.debug("Safe_mode selected for %s", self._name) @@ -540,16 +549,45 @@ class HomeKit: @callback def _async_register_bridge(self, dev_reg): """Register the bridge as a device so homekit_controller and exclude it from discovery.""" + formatted_mac = device_registry.format_mac(self.driver.state.mac) + # Connections and identifiers are both used here. + # + # connections exists so homekit_controller can know the + # virtual mac address of the bridge and know to not offer + # it via discovery. + # + # identifiers is used as well since the virtual mac may change + # because it will not survive manual pairing resets (deleting state file) + # which we have trained users to do over the past few years + # because this was the way you had to fix homekit when pairing + # failed. + # + connection = (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) + identifier = (DOMAIN, self._entry_id, BRIDGE_SERIAL_NUMBER) + self._async_purge_old_bridges(dev_reg, identifier, connection) dev_reg.async_get_or_create( config_entry_id=self._entry_id, - connections={ - (device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac) - }, + identifiers={identifier}, + connections={connection}, manufacturer=MANUFACTURER, name=self._name, model="Home Assistant HomeKit Bridge", ) + @callback + def _async_purge_old_bridges(self, dev_reg, identifier, connection): + """Purge bridges that exist from failed pairing or manual resets.""" + devices_to_purge = [] + for entry in dev_reg.devices.values(): + if self._entry_id in entry.config_entries and ( + identifier not in entry.identifiers + or connection not in entry.connections + ): + devices_to_purge.append(entry.id) + + for device_id in devices_to_purge: + dev_reg.async_remove_device(device_id) + def _start(self, bridged_states): from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel type_cameras, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index c0e2ea90fba..b016997b7c9 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -19,6 +19,7 @@ from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( AID_STORAGE, BRIDGE_NAME, + BRIDGE_SERIAL_NUMBER, CONF_AUTO_START, CONF_ENTRY_INDEX, CONF_SAFE_MODE, @@ -458,7 +459,7 @@ async def test_homekit_entity_filter(hass): assert mock_get_acc.called is False -async def test_homekit_start(hass, hk_driver, debounce_patcher): +async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -480,6 +481,15 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): homekit.driver = hk_driver homekit._filter = Mock(return_value=True) + connection = (device_registry.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF") + bridge_with_wrong_mac = device_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={connection}, + manufacturer="Any", + name="Any", + model="Home Assistant HomeKit Bridge", + ) + hass.states.async_set("light.demo", "on") state = hass.states.async_all()[0] @@ -505,6 +515,35 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): await hass.async_block_till_done() assert not hk_driver_start.called + assert device_reg.async_get(bridge_with_wrong_mac.id) is None + + device = device_reg.async_get_device( + {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {} + ) + assert device + formatted_mac = device_registry.format_mac(homekit.driver.state.mac) + assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections + + # Start again to make sure the registry entry is kept + homekit.status = STATUS_READY + with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( + f"{PATH_HOMEKIT}.show_setup_message" + ) as mock_setup_msg, patch( + "pyhap.accessory_driver.AccessoryDriver.add_accessory" + ) as hk_driver_add_acc, patch( + "pyhap.accessory_driver.AccessoryDriver.start" + ) as hk_driver_start: + await homekit.async_start() + + device = device_reg.async_get_device( + {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {} + ) + assert device + formatted_mac = device_registry.format_mac(homekit.driver.state.mac) + assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections + + assert len(device_reg.devices) == 1 + async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" From a1eb513bbc73cc852f84a21b00ff37ff9dd8190b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 25 May 2020 06:40:16 -0500 Subject: [PATCH 47/68] Update rokuecp to 0.4.2 (#36102) * update rokuecp to 0.4.2 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 57c64f4c64a..276fe2332f5 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.4.1"], + "requirements": ["rokuecp==0.4.2"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index 1a96e4ffb29..dbb470f1513 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1871,7 +1871,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.4.1 +rokuecp==0.4.2 # homeassistant.components.roomba roombapy==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 417937253ec..97848037bf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ rflink==0.0.52 ring_doorbell==0.6.0 # homeassistant.components.roku -rokuecp==0.4.1 +rokuecp==0.4.2 # homeassistant.components.roomba roombapy==1.6.1 From 79ed8b03dc9d8241a823294da3a466ac1fa110e7 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Mon, 25 May 2020 07:37:47 -0400 Subject: [PATCH 48/68] guard against missing topic (#36108) --- homeassistant/components/onvif/event.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 7a0113177c4..3888db4fa8e 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -147,6 +147,10 @@ class EventManager: async def async_parse_messages(self, messages) -> None: """Parse notification message.""" for msg in messages: + # Guard against empty message + if not msg.Topic: + continue + topic = msg.Topic._value_1 parser = PARSERS.get(topic) if not parser: From ef3019f9226e66f116185c704df89039cdc34f59 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Mon, 25 May 2020 07:38:57 -0400 Subject: [PATCH 49/68] fix preset warning (#36110) --- homeassistant/components/onvif/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index c4d3146002e..0e9d3ddca98 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -424,7 +424,7 @@ class ONVIFDevice: "PTZ preset '%s' does not exist on device '%s'. Available Presets: %s", preset_val, self.name, - profile.ptz.presets.join(", "), + ", ".join(profile.ptz.presets), ) return From 4c21adfd88ad616ac8ecb3fc5a8e5fe87f4853c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 May 2020 10:51:46 -0500 Subject: [PATCH 50/68] Remove unsupported stop feature with Hunter Douglas Powerview 1.0 Hubs (#36129) --- homeassistant/components/hunterdouglas_powerview/cover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 8135b4a8c77..ea56d56352a 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -28,6 +28,7 @@ from .const import ( DEVICE_INFO, DEVICE_MODEL, DOMAIN, + LEGACY_DEVICE_MODEL, PV_API, PV_ROOM_DATA, PV_SHADE_DATA, @@ -118,7 +119,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): def supported_features(self): """Flag supported features.""" supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - if self._device_info[DEVICE_MODEL] != "1": + if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL: supported_features |= SUPPORT_STOP return supported_features From 701f906898004ef7449b0b704b7e7a39910a2902 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 May 2020 12:39:24 -0700 Subject: [PATCH 51/68] Fix client ID lookup for official apps (#36131) --- homeassistant/components/auth/indieauth.py | 4 ++-- tests/components/auth/test_indieauth.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index cd8e797876f..0d942bd358d 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -33,8 +33,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): # Whitelist the iOS and Android callbacks so that people can link apps # without being connected to the internet. if redirect_uri == "homeassistant://auth-callback" and client_id in ( - "https://www.home-assistant.io/android", - "https://www.home-assistant.io/iOS", + "https://home-assistant.io/android", + "https://home-assistant.io/iOS", ): return True diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 3d1ba068c85..8a02502e16c 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -166,8 +166,7 @@ async def test_find_link_tag_max_size(hass, mock_session): @pytest.mark.parametrize( - "client_id", - ["https://www.home-assistant.io/android", "https://www.home-assistant.io/iOS"], + "client_id", ["https://home-assistant.io/android", "https://home-assistant.io/iOS"], ) async def test_verify_redirect_uri_android_ios(client_id): """Test that we verify redirect uri correctly for Android/iOS.""" From 73aca24f30d6bd8562546b929b4089e1ddf7f6d8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 26 May 2020 11:44:07 +0200 Subject: [PATCH 52/68] Update frontend to 20200519.5 (#36154) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 89f83d8fa65..14ae15dd87b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200519.4"], + "requirements": ["home-assistant-frontend==20200519.5"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e8c3550e996..52981f0a1a1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 hass-nabucasa==0.34.3 -home-assistant-frontend==20200519.4 +home-assistant-frontend==20200519.5 importlib-metadata==1.6.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index dbb470f1513..c1c0323a906 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.4 +home-assistant-frontend==20200519.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97848037bf2..8f75c4d8694 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.4 +home-assistant-frontend==20200519.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From cf585babbe3da3cb820c9d1ef7ddf542c6296894 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 May 2020 12:12:34 +0200 Subject: [PATCH 53/68] Bumped version to 0.110.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8993225f283..da3d94d85b9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 110 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From d1be3c82684fd8c06af600f6213c0db239e0062d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 May 2020 10:51:50 -0500 Subject: [PATCH 54/68] Add ability to ignore heos discovery (#34653) * Add ability to ignore heos discovery * Fetch player_id, update tests * Handle failure state * Update tests as there are two players in the mock now * Adjust and add more tests * Strip out player id lookup * reverts per review * one more revert --- homeassistant/components/heos/__init__.py | 4 ++ homeassistant/components/heos/config_flow.py | 4 ++ tests/components/heos/test_config_flow.py | 48 ++++++++++++++++++-- tests/components/heos/test_init.py | 2 + 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 53c65a6ab07..a76a29b2ed5 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -61,6 +61,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents the HEOS controller.""" + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) + host = entry.data[CONF_HOST] # Setting all_progress_events=False ensures that we only receive a # media position update upon start of playback or when media changes diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 91dbc19ac95..138d1c4462c 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -33,12 +33,16 @@ class HeosFlowHandler(config_entries.ConfigFlow): # Abort if other flows in progress or an entry already exists if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="already_setup") + await self.async_set_unique_id(DOMAIN) # Show selection form return self.async_show_form(step_id="user") async def async_step_import(self, user_input=None): """Occurs when an entry is setup through config.""" host = user_input[CONF_HOST] + # raise_on_progress is False here in case ssdp discovers + # heos first which would block the import + await self.async_set_unique_id(DOMAIN, raise_on_progress=False) return self.async_create_entry(title=format_title(host), data={CONF_HOST: host}) async def async_step_user(self, user_input=None): diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index d90c4263240..800814df013 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -6,7 +6,8 @@ from pyheos import HeosError from homeassistant import data_entry_flow from homeassistant.components import heos, ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler -from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS +from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST from tests.async_mock import patch @@ -55,6 +56,7 @@ async def test_create_entry_when_host_valid(hass, controller): heos.DOMAIN, context={"source": "user"}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data assert controller.connect.call_count == 1 @@ -70,6 +72,7 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): heos.DOMAIN, context={"source": "user"}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} assert controller.connect.call_count == 1 @@ -79,28 +82,34 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): async def test_discovery_shows_create_form(hass, controller, discovery_data): """Test discovery shows form to confirm setup and subsequent abort.""" + await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": "ssdp"}, data=discovery_data ) await hass.async_block_till_done() - assert len(hass.config_entries.flow.async_progress()) == 1 + flows_in_progress = hass.config_entries.flow.async_progress() + assert flows_in_progress[0]["context"]["unique_id"] == DOMAIN + assert len(flows_in_progress) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == {"Office (127.0.0.1)": "127.0.0.1"} port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" + await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": "ssdp"}, data=discovery_data ) await hass.async_block_till_done() - assert len(hass.config_entries.flow.async_progress()) == 1 + flows_in_progress = hass.config_entries.flow.async_progress() + assert flows_in_progress[0]["context"]["unique_id"] == DOMAIN + assert len(flows_in_progress) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == { "Office (127.0.0.1)": "127.0.0.1", "Bedroom (127.0.0.2)": "127.0.0.2", } -async def test_disovery_flow_aborts_already_setup( +async def test_discovery_flow_aborts_already_setup( hass, controller, discovery_data, config_entry ): """Test discovery flow aborts when entry already setup.""" @@ -110,3 +119,34 @@ async def test_disovery_flow_aborts_already_setup( result = await flow.async_step_ssdp(discovery_data) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" + + +async def test_discovery_sets_the_unique_id(hass, controller, discovery_data): + """Test discovery sets the unique id.""" + + port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port + discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" + discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" + + await hass.config_entries.flow.async_init( + heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data + ) + await hass.async_block_till_done() + flows_in_progress = hass.config_entries.flow.async_progress() + assert flows_in_progress[0]["context"]["unique_id"] == DOMAIN + assert len(flows_in_progress) == 1 + assert hass.data[DATA_DISCOVERED_HOSTS] == {"Bedroom (127.0.0.2)": "127.0.0.2"} + + +async def test_import_sets_the_unique_id(hass, controller): + """Test import sets the unique id.""" + + with patch("homeassistant.components.heos.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + heos.DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: "127.0.0.2"}, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == DOMAIN diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index a6852e3db41..a32ea5dd08c 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -31,6 +31,7 @@ async def test_async_setup_creates_entry(hass, config): entry = entries[0] assert entry.title == "Controller (127.0.0.1)" assert entry.data == {CONF_HOST: "127.0.0.1"} + assert entry.unique_id == DOMAIN async def test_async_setup_updates_entry(hass, config_entry, config, controller): @@ -44,6 +45,7 @@ async def test_async_setup_updates_entry(hass, config_entry, config, controller) entry = entries[0] assert entry.title == "Controller (127.0.0.2)" assert entry.data == {CONF_HOST: "127.0.0.2"} + assert entry.unique_id == DOMAIN async def test_async_setup_returns_true(hass, config_entry, config): From dc00fa9c27123d202a7bd0f431deb30579bdbd82 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 26 May 2020 17:02:18 -0700 Subject: [PATCH 55/68] Fix Android TV icon when screencap option is disabled (#35710) * Don't return a media image hash if the screencap config option is False * 1-liner --- homeassistant/components/androidtv/media_player.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 44085273940..4fd7b70835d 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -452,6 +452,11 @@ class ADBDevice(MediaPlayerEntity): """Provide the last ADB command's response as an attribute.""" return {"adb_response": self._adb_response} + @property + def media_image_hash(self): + """Hash value for media image.""" + return f"{datetime.now().timestamp()}" if self._screencap else None + @property def name(self): """Return the device name.""" @@ -497,11 +502,6 @@ class ADBDevice(MediaPlayerEntity): """Raw image data.""" return self.aftv.adb_screencap() - @property - def media_image_hash(self): - """Hash value for media image.""" - return f"{datetime.now().timestamp()}" - @adb_decorator() def media_play(self): """Send play command.""" From 2df1c90e6a4c9dd876e27942ccc477bfe8528beb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 26 May 2020 17:12:22 +0200 Subject: [PATCH 56/68] Let PAHO MQTT client handle connection to MQTT server (#35983) * Let PAHO client handle connection to MQTT server --- homeassistant/components/mqtt/__init__.py | 28 ++++++++--------------- tests/components/mqtt/test_init.py | 12 +++++----- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 25b2b0381ea..64b25ad9486 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -29,11 +29,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, ServiceCall, callback -from homeassistant.exceptions import ( - ConfigEntryNotReady, - HomeAssistantError, - Unauthorized, -) +from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -649,13 +645,7 @@ async def async_setup_entry(hass, entry): tls_version=tls_version, ) - result: str = await hass.data[DATA_MQTT].async_connect() - - if result == CONNECTION_FAILED: - return False - - if result == CONNECTION_FAILED_RECOVERABLE: - raise ConfigEntryNotReady + await hass.data[DATA_MQTT].async_connect() async def async_stop_mqtt(_event: Event): """Stop MQTT component.""" @@ -835,15 +825,14 @@ class MQTT: self._mqttc.connect, self.broker, self.port, self.keepalive ) except OSError as err: - _LOGGER.error("Failed to connect due to exception: %s", err) - return CONNECTION_FAILED_RECOVERABLE + _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) - if result != 0: - _LOGGER.error("Failed to connect: %s", mqtt.error_string(result)) - return CONNECTION_FAILED + if result is not None and result != 0: + _LOGGER.error( + "Failed to connect to MQTT server: %s", mqtt.error_string(result) + ) self._mqttc.loop_start() - return CONNECTION_SUCCESS async def async_disconnect(self): """Stop the MQTT client.""" @@ -933,6 +922,7 @@ class MQTT: return self.connected = True + _LOGGER.info("Connected to MQTT server (%s)", result_code) # Group subscriptions to only re-subscribe once for each topic. keyfunc = attrgetter("topic") @@ -999,7 +989,7 @@ class MQTT: def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" self.connected = False - _LOGGER.warning("Disconnected from MQTT (%s).", result_code) + _LOGGER.warning("Disconnected from MQTT server (%s)", result_code) def _raise_on_error(result_code: int) -> None: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9ec5e09f276..3626c5a746c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -18,7 +18,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -678,23 +677,24 @@ async def test_setup_embedded_with_embedded(hass): assert _start.call_count == 1 -async def test_setup_fails_if_no_connect_broker(hass): +async def test_setup_logs_error_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 1 - assert not await mqtt.async_setup_entry(hass, entry) + assert await mqtt.async_setup_entry(hass, entry) + assert "Failed to connect to MQTT server:" in caplog.text -async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass): +async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = MagicMock(side_effect=OSError("Connection error")) - with pytest.raises(ConfigEntryNotReady): - await mqtt.async_setup_entry(hass, entry) + assert await mqtt.async_setup_entry(hass, entry) + assert "Failed to connect to MQTT server due to exception:" in caplog.text async def test_setup_uses_certificate_on_certificate_set_to_auto(hass, mock_mqtt): From 4df8e51edd7b1b5c11fe5f8ecacc7d018b836cd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 May 2020 11:05:41 -0500 Subject: [PATCH 57/68] Use a single service browser for zeroconf discovery (#35997) --- homeassistant/components/zeroconf/__init__.py | 9 +++++---- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zeroconf/test_init.py | 7 ++++--- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index d699160eed4..003686521a8 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -97,7 +97,7 @@ class HaServiceBrowser(ServiceBrowser): # To avoid overwhemling the system we pre-filter here and only process # DNSPointers for the configured record name (type) # - if record.name != self.type or not isinstance(record, DNSPointer): + if record.name not in self.types or not isinstance(record, DNSPointer): return super().update_record(zc, now, record) @@ -216,11 +216,12 @@ def setup(hass, config): ) ) - for service in ZEROCONF: - HaServiceBrowser(zeroconf, service, handlers=[service_update]) + types = list(ZEROCONF) if HOMEKIT_TYPE not in ZEROCONF: - HaServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) + types.append(HOMEKIT_TYPE) + + HaServiceBrowser(zeroconf, types, handlers=[service_update]) return True diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index a3d4d1d8399..9d4398f3b56 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.26.1"], + "requirements": ["zeroconf==0.26.2"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 52981f0a1a1..a33e1c84195 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.16 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.26.1 +zeroconf==0.26.2 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index c1c0323a906..5aad7209e6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2239,7 +2239,7 @@ youtube_dl==2020.05.08 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.26.1 +zeroconf==0.26.2 # homeassistant.components.zha zha-quirks==0.0.39 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f75c4d8694..29ec0a0e666 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -900,7 +900,7 @@ xmltodict==0.12.0 ya_ma==0.3.8 # homeassistant.components.zeroconf -zeroconf==0.26.1 +zeroconf==0.26.2 # homeassistant.components.zha zha-quirks==0.0.39 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 89c9d0c2643..74069fa5faa 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -29,9 +29,10 @@ def mock_zeroconf(): yield mock_zc.return_value -def service_update_mock(zeroconf, service, handlers): +def service_update_mock(zeroconf, services, handlers): """Call service update handler.""" - handlers[0](zeroconf, service, f"name.{service}", ServiceStateChange.Added) + for service in services: + handlers[0](zeroconf, service, f"name.{service}", ServiceStateChange.Added) def get_service_info_mock(service_type, name): @@ -76,7 +77,7 @@ async def test_setup(hass, mock_zeroconf): mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF) + assert len(mock_service_browser.mock_calls) == 1 expected_flow_calls = 0 for matching_components in zc_gen.ZEROCONF.values(): expected_flow_calls += len(matching_components) From e6e7d0651dbb4b697449ead1cd82918cd96fccd5 Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Tue, 26 May 2020 15:39:53 +0100 Subject: [PATCH 58/68] Check todoist due date is not None in async_get_events (#36140) * Check that due date is not None Check that due date is not None, prevents taks without due dates from breaking Calendar API * Invert None check to reduce indentation --- homeassistant/components/todoist/calendar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index fd246520696..0ce8101f49c 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -500,6 +500,8 @@ class TodoistProjectData: events = [] for task in project_task_data: + if task["due"] is None: + continue due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { From d4f7fe16ca2a9704e4e1b8634c8e99c8cfc0436a Mon Sep 17 00:00:00 2001 From: Markus Bong Date: Tue, 26 May 2020 15:56:49 +0200 Subject: [PATCH 59/68] Fix cloud connection within API (#36158) --- homeassistant/components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index e3a4e2f8720..867848f89d1 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo_home_control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.10.0"], + "requirements": ["devolo-home-control-api==0.11.0"], "config_flow": true, "codeowners": [ "@2Fake", diff --git a/requirements_all.txt b/requirements_all.txt index 5aad7209e6d..b23b4f9612f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -469,7 +469,7 @@ deluge-client==1.7.1 denonavr==0.8.1 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.10.0 +devolo-home-control-api==0.11.0 # homeassistant.components.directv directv==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29ec0a0e666..36443fd8e01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -203,7 +203,7 @@ defusedxml==0.6.0 denonavr==0.8.1 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.10.0 +devolo-home-control-api==0.11.0 # homeassistant.components.directv directv==0.3.0 From 0865791116499fea518ed99bd94b6f0bf63663ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 May 2020 22:11:40 -0700 Subject: [PATCH 60/68] Revert DSMR not calling entity methods (#36179) --- homeassistant/components/dsmr/sensor.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 484bd708489..4d780d48cd1 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -17,10 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -113,7 +109,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) - update_entities_telegram = partial(async_dispatcher_send, hass, DOMAIN) + def update_entities_telegram(telegram): + """Update entities with latest telegram and trigger state update.""" + # Make all device entities aware of new telegram + for device in devices: + device.update_data(telegram) # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival @@ -187,17 +187,12 @@ class DSMREntity(Entity): self._config = config self.telegram = {} - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.update_data) - ) - @callback def update_data(self, telegram): """Update data.""" self.telegram = telegram - self.async_write_ha_state() + if self.hass: + self.async_write_ha_state() def get_dsmr_object_attr(self, attribute): """Read attribute from last received telegram for this DSMR object.""" From c710f9994c231195c38e8cb3c1a1dbe83468adf7 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 27 May 2020 00:16:15 -0400 Subject: [PATCH 61/68] Fix empty preset element in ONVIF response (#36182) --- homeassistant/components/onvif/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 0e9d3ddca98..bbce71f5d28 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -297,7 +297,7 @@ class ONVIFDevice: try: ptz_service = self.device.create_ptz_service() presets = await ptz_service.GetPresets(profile.token) - profile.ptz.presets = [preset.token for preset in presets] + profile.ptz.presets = [preset.token for preset in presets if preset] except (Fault, ServerDisconnectedError): # It's OK if Presets aren't supported profile.ptz.presets = [] From 61d8efae5f7e06175ce0255bbf0149beddfc95f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 May 2020 11:09:21 -0500 Subject: [PATCH 62/68] Update cast to use shared zeroconf (#35570) * Update cast to use the shared zeroconf instance * Add zeroconf to after_dependencies * Bump version to 5.2.0 --- homeassistant/components/cast/discovery.py | 5 +++-- homeassistant/components/cast/manifest.json | 4 ++-- homeassistant/components/cast/media_player.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 9f90b074c3d..b15346417de 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -85,9 +85,10 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery( - internal_add_callback, internal_remove_callback + internal_add_callback, + internal_remove_callback, + ChromeCastZeroconf.get_zeroconf(), ) - ChromeCastZeroconf.set_zeroconf(browser.zc) def stop_discovery(event): """Stop discovery of new chromecasts.""" diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index ddb6697d370..baeeef6de65 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,8 +3,8 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==5.1.0"], - "after_dependencies": ["cloud"], + "requirements": ["pychromecast==5.2.0"], + "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index fd7443d1c26..84917e0194a 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -14,6 +14,7 @@ from pychromecast.socket_client import ( ) import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, @@ -170,6 +171,7 @@ async def _async_setup_platform( for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): async_cast_discovered(chromecast) + ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) hass.async_add_executor_job(setup_internal_discovery, hass) diff --git a/requirements_all.txt b/requirements_all.txt index b23b4f9612f..a5e6d82e430 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1245,7 +1245,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==5.1.0 +pychromecast==5.2.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36443fd8e01..09e7d7e543d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -527,7 +527,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==5.1.0 +pychromecast==5.2.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 From 3a80cb1d1df1f80f68206ebee1437ffa5f9305c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 May 2020 13:31:26 +0200 Subject: [PATCH 63/68] Bump zeroconf, pychromecast. Log if zeroconf.get_service_info fails. (#36185) --- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/zeroconf/__init__.py | 1 + homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index baeeef6de65..0be595de549 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==5.2.0"], + "requirements": ["pychromecast==5.3.0"], "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 003686521a8..c2fcb88c305 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -181,6 +181,7 @@ def setup(hass, config): if not service_info: # Prevent the browser thread from collapsing as # service_info can be None + _LOGGER.debug("Failed to get info for device %s", name) return info = info_from_service(service_info) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 9d4398f3b56..e28594d5598 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.26.2"], + "requirements": ["zeroconf==0.26.3"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a33e1c84195..0a0fc81ae5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.16 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.26.2 +zeroconf==0.26.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index a5e6d82e430..01d99251e22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1245,7 +1245,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==5.2.0 +pychromecast==5.3.0 # homeassistant.components.cmus pycmus==0.1.1 @@ -2239,7 +2239,7 @@ youtube_dl==2020.05.08 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.26.2 +zeroconf==0.26.3 # homeassistant.components.zha zha-quirks==0.0.39 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09e7d7e543d..79af61a03e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -527,7 +527,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==5.2.0 +pychromecast==5.3.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 @@ -900,7 +900,7 @@ xmltodict==0.12.0 ya_ma==0.3.8 # homeassistant.components.zeroconf -zeroconf==0.26.2 +zeroconf==0.26.3 # homeassistant.components.zha zha-quirks==0.0.39 From e967c93b7bfab65afa26ba3d9febe7e26f456b0e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 27 May 2020 10:53:26 -0500 Subject: [PATCH 64/68] Fix roku select source with app ids (#36191) --- homeassistant/components/roku/media_player.py | 7 ++++++- tests/components/roku/test_media_player.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 0deeb44dbc2..69c9e24ad89 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -222,7 +222,12 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): await self.coordinator.roku.remote("home") appl = next( - (app for app in self.coordinator.data.apps if app.name == source), None + ( + app + for app in self.coordinator.data.apps + if source in (app.name, app.app_id) + ), + None, ) if appl is not None: diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 9d809cae433..9ac758585e9 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -332,7 +332,7 @@ async def test_services( remote_mock.assert_called_once_with("home") - with patch("homeassistant.components.roku.Roku.launch") as remote_mock: + with patch("homeassistant.components.roku.Roku.launch") as launch_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -340,7 +340,17 @@ async def test_services( blocking=True, ) - remote_mock.assert_called_once_with("12") + launch_mock.assert_called_once_with("12") + + with patch("homeassistant.components.roku.Roku.launch") as launch_mock: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: 12}, + blocking=True, + ) + + launch_mock.assert_called_once_with("12") async def test_tv_services( From ca0149c6355efb5eadda7006dcf1febede00837e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 27 May 2020 13:53:14 -0600 Subject: [PATCH 65/68] Prevent AirVisual from polling (#36199) * Prevent AirVisual from polling * Docstring --- homeassistant/components/airvisual/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 96b3770974a..8d41bd359b8 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -375,6 +375,11 @@ class AirVisualEntity(Entity): """Return the icon.""" return self._icon + @property + def should_poll(self) -> bool: + """Disable polling.""" + return False + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" From 8426bdc9ae75bcc9934acc3c716c6c448b12bb5a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 May 2020 19:37:09 +0200 Subject: [PATCH 66/68] Fix custom position range (#36222) --- homeassistant/components/mqtt/cover.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 4ed128f1ff3..c9fc388e1a3 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -530,7 +530,7 @@ class MqttCover( async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - position = float(kwargs[ATTR_TILT_POSITION]) + position = kwargs[ATTR_TILT_POSITION] # The position needs to be between min and max level = self.find_in_range_from_percent(position) @@ -550,10 +550,7 @@ class MqttCover( percentage_position = position if set_position_template is not None: position = set_position_template.async_render(**kwargs) - elif ( - self._config[CONF_POSITION_OPEN] != 100 - and self._config[CONF_POSITION_CLOSED] != 0 - ): + else: position = self.find_in_range_from_percent(position, COVER_PAYLOAD) mqtt.async_publish( From 093d797c3b129828c844659365a39232675946ce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 May 2020 00:50:23 +0200 Subject: [PATCH 67/68] Correct MQTT device trigger reconfiguration with same topic (#36234) --- homeassistant/components/mqtt/__init__.py | 1 + .../components/mqtt/device_trigger.py | 12 ++-- tests/components/mqtt/test_device_trigger.py | 62 ++++++++++++++++++- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 64b25ad9486..a1eaf2f3a2a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -887,6 +887,7 @@ class MQTT: This method is a coroutine. """ + _LOGGER.debug("Unsubscribing from %s", topic) async with self._paho_lock: result: int = None result, _ = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 3b65243d078..7ff40ffe27d 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -138,13 +138,17 @@ class Trigger: self.remove_signal = remove_signal self.type = config[CONF_TYPE] self.subtype = config[CONF_SUBTYPE] - self.topic = config[CONF_TOPIC] self.payload = config[CONF_PAYLOAD] self.qos = config[CONF_QOS] + topic_changed = self.topic != config[CONF_TOPIC] + self.topic = config[CONF_TOPIC] - # Unsubscribe+subscribe if this trigger is in use - for trig in self.trigger_instances: - await trig.async_attach_trigger() + # Unsubscribe+subscribe if this trigger is in use and topic has changed + # If topic is same unsubscribe+subscribe will execute in the wrong order + # because unsubscribe is done with help of async_create_task + if topic_changed: + for trig in self.trigger_instances: + await trig.async_attach_trigger() def detach_trigger(self): """Remove MQTT device trigger.""" diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index f77ccca57ef..aa7dc746951 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -14,6 +14,7 @@ from tests.common import ( assert_lists_same, async_fire_mqtt_message, async_get_device_automations, + async_mock_mqtt_component, async_mock_service, mock_device_registry, mock_registry, @@ -456,7 +457,7 @@ async def test_if_fires_on_mqtt_message_after_update( await hass.async_block_till_done() assert len(calls) == 1 - # Update the trigger + # Update the trigger with different topic async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data2) await hass.async_block_till_done() @@ -468,6 +469,65 @@ async def test_if_fires_on_mqtt_message_after_update( await hass.async_block_till_done() assert len(calls) == 2 + # Update the trigger with same topic + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data2) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "foobar/triggers/button1", "") + await hass.async_block_till_done() + assert len(calls) == 2 + + async_fire_mqtt_message(hass, "foobar/triggers/buttonOne", "") + await hass.async_block_till_done() + assert len(calls) == 3 + + +async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): + """Test subscription to topics without change.""" + mock_mqtt = await async_mock_mqtt_component(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + ' "topic": "foobar/triggers/button1",' + ' "type": "button_short_press",' + ' "subtype": "button_1" }' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla1", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press")}, + }, + }, + ] + }, + ) + + call_count = mock_mqtt.async_subscribe.call_count + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + assert mock_mqtt.async_subscribe.call_count == call_count + async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass, device_reg, calls, mqtt_mock From 33f645aba96c54f6f0fcd8569182741697854e58 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 May 2020 15:52:40 -0700 Subject: [PATCH 68/68] Bumped version to 0.110.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index da3d94d85b9..1c49e3447e6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 110 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0)