From 3ae82c3cacd5715d6b579e1aca4171e8ab5758b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:31:55 -1000 Subject: [PATCH 01/10] Bump aiodiscover to 1.3.4 (#49142) - Changelog: https://github.com/bdraco/aiodiscover/compare/v1.3.3...v1.3.4 (bumps pyroute2>=0.5.18 to fix https://github.com/svinota/pyroute2/issues/717) --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 80cc6b116c9..e841fb6bebb 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,7 @@ "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.3" + "scapy==2.4.4", "aiodiscover==1.3.4" ], "codeowners": [ "@bdraco" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5f0ccde05d..313c2af3a70 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.3.3 +aiodiscover==1.3.4 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==1.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4831b310f8e..ca36481bfed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.3 +aiodiscover==1.3.4 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33ef9a95aa8..5ee10757f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.3 +aiodiscover==1.3.4 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 02def469914d94c5a0c8fb61af951d0cec02c13a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:44:07 -1000 Subject: [PATCH 02/10] Fix setting up remotes that lack a supported features list in homekit (#49152) --- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_config_flow.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index a746355e124..673abc5da67 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -507,5 +507,5 @@ def state_needs_accessory_mode(state): or state.domain == MEDIA_PLAYER_DOMAIN and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV or state.domain == REMOTE_DOMAIN - and state.attributes.get(ATTR_SUPPORTED_FEATURES) & SUPPORT_ACTIVITY + and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & SUPPORT_ACTIVITY ) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 26804675265..c06e8aaa5ad 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -145,6 +145,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): hass.states.async_set("camera.one", "on") hass.states.async_set("camera.existing", "on") hass.states.async_set("media_player.two", "on", {"device_class": "tv"}) + hass.states.async_set("remote.standard", "on") + hass.states.async_set("remote.activity", "on", {"supported_features": 4}) bridge_mode_entry = MockConfigEntry( domain=DOMAIN, @@ -178,7 +180,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"include_domains": ["camera", "media_player", "light"]}, + {"include_domains": ["camera", "media_player", "light", "remote"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "pairing" @@ -205,7 +207,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": ["media_player", "light"], + "include_domains": ["media_player", "light", "remote"], "include_entities": [], }, "exclude_accessory_mode": True, @@ -222,7 +224,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): # 3 - new bridge # 4 - camera.one in accessory mode # 5 - media_player.two in accessory mode - assert len(mock_setup_entry.mock_calls) == 5 + # 6 - remote.activity in accessory mode + assert len(mock_setup_entry.mock_calls) == 6 async def test_import(hass): From 5356ce2b6d7352b652139c68689846f77c11e966 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Apr 2021 15:09:50 +0200 Subject: [PATCH 03/10] Don't receive homeassistant_* events from MQTT eventstream (#49158) --- .../components/mqtt_eventstream/__init__.py | 22 +++++++++++-- .../components/mqtt_eventstream/test_init.py | 31 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index 328b9395eea..d31d6d1cd53 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -7,6 +7,11 @@ from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_t from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, @@ -37,6 +42,14 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +BLOCKED_EVENTS = [ + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_FINAL_WRITE, +] + async def async_setup(hass, config): """Set up the MQTT eventstream component.""" @@ -45,16 +58,15 @@ async def async_setup(hass, config): pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) ignore_event = conf.get(CONF_IGNORE_EVENT) + ignore_event.append(EVENT_TIME_CHANGED) @callback def _event_publisher(event): """Handle events by publishing them on the MQTT queue.""" if event.origin != EventOrigin.local: return - if event.event_type == EVENT_TIME_CHANGED: - return - # User-defined events to ignore + # Events to ignore if event.event_type in ignore_event: return @@ -84,6 +96,10 @@ async def async_setup(hass, config): event_type = event.get("event_type") event_data = event.get("event_data") + # Don't fire HOMEASSISTANT_* events on this instance + if event_type in BLOCKED_EVENTS: + return + # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects # Copied over from the _handle_api_post_events_event method diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 7f6b22bda90..6a1633cb111 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -3,7 +3,7 @@ import json from unittest.mock import ANY, patch import homeassistant.components.mqtt_eventstream as eventstream -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL from homeassistant.core import State, callback from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -114,6 +114,7 @@ async def test_time_event_does_not_send_message(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() async_fire_time_changed(hass, dt_util.utcnow()) + await hass.async_block_till_done() assert not mqtt_mock.async_publish.called @@ -140,6 +141,33 @@ async def test_receiving_remote_event_fires_hass_event(hass, mqtt_mock): assert len(calls) == 1 + await hass.async_block_till_done() + + +async def test_receiving_blocked_event_fires_hass_event(hass, mqtt_mock): + """Test the receiving of blocked event does not fire.""" + sub_topic = "foo" + assert await add_eventstream(hass, sub_topic=sub_topic) + await hass.async_block_till_done() + + calls = [] + + @callback + def listener(_): + calls.append(1) + + hass.bus.async_listen(MATCH_ALL, listener) + await hass.async_block_till_done() + + for event in eventstream.BLOCKED_EVENTS: + payload = json.dumps({"event_type": event, "event_data": {}}, cls=JSONEncoder) + async_fire_mqtt_message(hass, sub_topic, payload) + await hass.async_block_till_done() + + assert len(calls) == 0 + + await hass.async_block_till_done() + async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock): """Test the ignoring of sending events if defined.""" @@ -159,6 +187,7 @@ async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock): # Set a state of an entity mock_state_change_event(hass, State(e_id, "on")) await hass.async_block_till_done() + await hass.async_block_till_done() assert not mqtt_mock.async_publish.called From a3ca48c1bda811330a527e8f108c05751d185e84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Apr 2021 17:24:21 +0200 Subject: [PATCH 04/10] Set deprecated supported_features for MQTT JSON light (#49167) * Set deprecated supported_features for MQTT json light * Update homeassistant/components/light/__init__.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/light/__init__.py | 17 +++++ .../components/mqtt/light/schema_json.py | 5 +- tests/components/mqtt/test_light_json.py | 71 +++++++++++++++---- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fe9a38d12b4..bfdb723e159 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -751,3 +751,20 @@ class Light(LightEntity): "Light is deprecated, modify %s to extend LightEntity", cls.__name__, ) + + +def legacy_supported_features( + supported_features: int, supported_color_modes: list[str] | None +) -> int: + """Calculate supported features with backwards compatibility.""" + # Backwards compatibility for supported_color_modes added in 2021.4 + if supported_color_modes is None: + return supported_features + if any(mode in supported_color_modes for mode in COLOR_MODES_COLOR): + supported_features |= SUPPORT_COLOR + if any(mode in supported_color_modes for mode in COLOR_MODES_BRIGHTNESS): + supported_features |= SUPPORT_BRIGHTNESS + if COLOR_MODE_COLOR_TEMP in supported_color_modes: + supported_features |= SUPPORT_COLOR_TEMP + + return supported_features diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index aaf12f3362f..9940d646a35 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -35,6 +35,7 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, LightEntity, + legacy_supported_features, valid_supported_color_modes, ) from homeassistant.const import ( @@ -458,7 +459,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @property def supported_features(self): """Flag supported features.""" - return self._supported_features + return legacy_supported_features( + self._supported_features, self._config.get(CONF_SUPPORTED_COLOR_MODES) + ) def _set_flash_and_transition(self, message, **kwargs): if ATTR_TRANSITION in kwargs: diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 6c9c7ae903a..77e5936c7b4 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -234,10 +234,10 @@ async def test_rgb_light(hass, mqtt_mock): state = hass.states.get("light.test") expected_features = ( - light.SUPPORT_TRANSITION + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_FLASH - | light.SUPPORT_BRIGHTNESS + | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features @@ -261,7 +261,8 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -310,7 +311,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -429,7 +439,15 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") is None assert state.attributes.get("color_mode") is None assert state.attributes.get("color_temp") is None @@ -610,7 +628,16 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 assert state.attributes.get("white_value") == 50 - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "light.test") @@ -738,7 +765,15 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") == 95 assert state.attributes.get("color_mode") == "rgb" assert state.attributes.get("color_temp") is None @@ -1313,7 +1348,10 @@ async def test_effect(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test") @@ -1373,7 +1411,8 @@ async def test_flash_short_and_long(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", flash="short") @@ -1431,8 +1470,8 @@ async def test_transition(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 - + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", transition=15) mqtt_mock.async_publish.assert_called_once_with( @@ -1523,7 +1562,15 @@ async def test_invalid_values(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 187 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("white_value") is None From 5c3cb044d03414d757f95e256c91e00e72e0b412 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Apr 2021 23:52:10 +0200 Subject: [PATCH 05/10] Upgrade spotipy to 2.18.0 (#49220) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index bd92217e9cf..d0d40291fff 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.17.1"], + "requirements": ["spotipy==2.18.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index ca36481bfed..df61a7be783 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2117,7 +2117,7 @@ spiderpy==1.4.2 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.17.1 +spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ee10757f4a..5c2fdb06cb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1104,7 +1104,7 @@ speedtest-cli==2.1.3 spiderpy==1.4.2 # homeassistant.components.spotify -spotipy==2.17.1 +spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql From ba0d3aad1c9a413f1d58933d8b8f00de724f24b3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Apr 2021 09:03:34 +0200 Subject: [PATCH 06/10] Fix race when restarting script (#49247) --- homeassistant/helpers/script.py | 25 +++++++++++++------ tests/components/automation/test_blueprint.py | 12 ++++----- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index bf52fc81b6a..52be3866639 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1144,10 +1144,7 @@ class Script: self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) script_execution_set("failed_single") return - if self.script_mode == SCRIPT_MODE_RESTART: - self._log("Restarting") - await self.async_stop(update_state=False) - elif len(self._runs) == self.max_runs: + if self.script_mode != SCRIPT_MODE_RESTART and self.runs == self.max_runs: if self._max_exceeded != "SILENT": self._log( "Maximum number of runs exceeded", @@ -1186,6 +1183,14 @@ class Script: self._hass, self, cast(dict, variables), context, self._log_exceptions ) self._runs.append(run) + if self.script_mode == SCRIPT_MODE_RESTART: + # When script mode is SCRIPT_MODE_RESTART, first add the new run and then + # stop any other runs. If we stop other runs first, self.is_running will + # return false after the other script runs were stopped until our task + # resumes running. + self._log("Restarting") + await self.async_stop(update_state=False, spare=run) + if started_action: self._hass.async_run_job(started_action) self.last_triggered = utcnow() @@ -1198,17 +1203,21 @@ class Script: self._changed() raise - async def _async_stop(self, update_state): - aws = [asyncio.create_task(run.async_stop()) for run in self._runs] + async def _async_stop(self, update_state, spare=None): + aws = [ + asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + ] if not aws: return await asyncio.wait(aws) if update_state: self._changed() - async def async_stop(self, update_state: bool = True) -> None: + async def async_stop( + self, update_state: bool = True, spare: _ScriptRun | None = None + ) -> None: """Stop running script.""" - await asyncio.shield(self._async_stop(update_state)) + await asyncio.shield(self._async_stop(update_state, spare)) async def _async_get_condition(self, config): if isinstance(config, template.Template): diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 747e162fe46..e035c238383 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -156,8 +156,8 @@ async def test_motion_light(hass): # Turn on motion hass.states.async_set("binary_sensor.kitchen", "on") # Can't block till done because delay is active - # So wait 5 event loop iterations to process script - for _ in range(5): + # So wait 10 event loop iterations to process script + for _ in range(10): await asyncio.sleep(0) assert len(turn_on_calls) == 1 @@ -165,7 +165,7 @@ async def test_motion_light(hass): # Test light doesn't turn off if motion stays async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) assert len(turn_off_calls) == 0 @@ -173,7 +173,7 @@ async def test_motion_light(hass): # Test light turns off off 120s after last motion hass.states.async_set("binary_sensor.kitchen", "off") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120)) @@ -184,7 +184,7 @@ async def test_motion_light(hass): # Test restarting the script hass.states.async_set("binary_sensor.kitchen", "on") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) assert len(turn_on_calls) == 2 @@ -192,7 +192,7 @@ async def test_motion_light(hass): hass.states.async_set("binary_sensor.kitchen", "off") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) hass.states.async_set("binary_sensor.kitchen", "on") From 733a394c554b0d29f88a033bbd4e8f4b7c653068 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 15 Apr 2021 16:12:49 +0200 Subject: [PATCH 07/10] Fix mysensors sensor protocol version check (#49257) --- homeassistant/components/mysensors/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a62318aea53..1a5f7330ddf 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,6 +1,8 @@ """Support for MySensors sensors.""" from typing import Callable +from awesomeversion import AwesomeVersion + from homeassistant.components import mysensors from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY @@ -115,7 +117,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Return the unit of measurement of this entity.""" set_req = self.gateway.const.SetReq if ( - float(self.gateway.protocol_version) >= 1.5 + AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5") and set_req.V_UNIT_PREFIX in self._values ): return self._values[set_req.V_UNIT_PREFIX] From 41736c93a1fd46605d70f8cb5db23c5028d138b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 10:53:19 +0200 Subject: [PATCH 08/10] Fix Coronavirus integration robustness (#49287) Co-authored-by: Martin Hjelmare --- .../components/coronavirus/__init__.py | 18 ++++++++----- .../components/coronavirus/config_flow.py | 13 ++++++++-- .../components/coronavirus/strings.json | 1 + .../coronavirus/translations/en.json | 3 ++- .../coronavirus/test_config_flow.py | 26 ++++++++++++++++++- tests/components/coronavirus/test_init.py | 25 +++++++++++++++++- 6 files changed, 74 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index d05c4cef862..4bda4edcd37 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -15,14 +15,14 @@ from .const import DOMAIN PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Coronavirus component.""" # Make sure coordinator is initialized. await get_coordinator(hass) return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coronavirus from a config entry.""" if isinstance(entry.data["country"], int): hass.config_entries.async_update_entry( @@ -44,6 +44,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"]) + coordinator = await get_coordinator(hass) + if not coordinator.last_update_success: + await coordinator.async_config_entry_first_refresh() + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) @@ -52,9 +56,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( + return all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, platform) @@ -63,10 +67,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) - return unload_ok - -async def get_coordinator(hass): +async def get_coordinator( + hass: HomeAssistant, +) -> update_coordinator.DataUpdateCoordinator: """Get the data update coordinator.""" if DOMAIN in hass.data: return hass.data[DOMAIN] diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 6d2776c7ecc..4f6e865fa37 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -1,4 +1,8 @@ """Config flow for Coronavirus integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -15,13 +19,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _options = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle the initial step.""" errors = {} if self._options is None: - self._options = {OPTION_WORLDWIDE: "Worldwide"} coordinator = await get_coordinator(self.hass) + if not coordinator.last_update_success: + return self.async_abort(reason="cannot_connect") + + self._options = {OPTION_WORLDWIDE: "Worldwide"} for case in sorted( coordinator.data.values(), key=lambda case: case.country ): diff --git a/homeassistant/components/coronavirus/strings.json b/homeassistant/components/coronavirus/strings.json index 6a5b2626003..e0b29d6c8db 100644 --- a/homeassistant/components/coronavirus/strings.json +++ b/homeassistant/components/coronavirus/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } diff --git a/homeassistant/components/coronavirus/translations/en.json b/homeassistant/components/coronavirus/translations/en.json index cbd057bfce1..ea7ba1f6f9d 100644 --- a/homeassistant/components/coronavirus/translations/en.json +++ b/homeassistant/components/coronavirus/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "cannot_connect": "Failed to connect" }, "step": { "user": { diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py index 06d586ba2a5..bfc69200893 100644 --- a/tests/components/coronavirus/test_config_flow.py +++ b/tests/components/coronavirus/test_config_flow.py @@ -1,9 +1,14 @@ """Test the Coronavirus config flow.""" +from unittest.mock import MagicMock, patch + +from aiohttp import ClientError + from homeassistant import config_entries, setup from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.core import HomeAssistant -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -24,3 +29,22 @@ async def test_form(hass): } await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 + + +@patch( + "coronavirus.get_cases", + side_effect=ClientError, +) +async def test_abort_on_connection_error( + mock_get_cases: MagicMock, hass: HomeAssistant +) -> None: + """Test we abort on connection error.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert "type" in result + assert result["type"] == "abort" + assert "reason" in result + assert result["reason"] == "cannot_connect" diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py index cc49bf7d4b6..c36255db9d1 100644 --- a/tests/components/coronavirus/test_init.py +++ b/tests/components/coronavirus/test_init.py @@ -1,12 +1,18 @@ """Test init of Coronavirus integration.""" +from unittest.mock import MagicMock, patch + +from aiohttp import ClientError + from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_registry -async def test_migration(hass): +async def test_migration(hass: HomeAssistant) -> None: """Test that we can migrate coronavirus to stable unique ID.""" nl_entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) nl_entry.add_to_hass(hass) @@ -47,3 +53,20 @@ async def test_migration(hass): assert nl_entry.unique_id == "Netherlands" assert worldwide_entry.unique_id == OPTION_WORLDWIDE + + +@patch( + "coronavirus.get_cases", + side_effect=ClientError, +) +async def test_config_entry_not_ready( + mock_get_cases: MagicMock, hass: HomeAssistant +) -> None: + """Test the configuration entry not ready.""" + entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY From 58b743eec1e8ad12b3fcf8d14607410b276565e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 16 Apr 2021 15:00:21 +0200 Subject: [PATCH 09/10] Mark camera as a base platform (#49297) --- homeassistant/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 1b48efb8c0f..778f3ea9b55 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -22,6 +22,7 @@ BASE_PLATFORMS = { "air_quality", "alarm_control_panel", "binary_sensor", + "camera", "climate", "cover", "device_tracker", From a08df4e18f0a8b65978fd8fd526586bfe97a6a02 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 15:35:06 +0200 Subject: [PATCH 10/10] Bumped version to 2021.4.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 875e60e2202..9ebf7516628 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 4 -PATCH_VERSION = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0)