From 8d9bb73d22a5b0b014dce709e5f566f334b1668c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jan 2022 20:10:35 +0100 Subject: [PATCH 001/298] Bumped version to 2022.2.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1c89dae3256..acd93a3ccf4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) From 63048a67e079b2ade309e3d6cb2c55f8061caea9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jan 2022 08:41:27 +0100 Subject: [PATCH 002/298] Fix MQTT climate action null warnings (#64658) --- homeassistant/components/mqtt/climate.py | 8 ++++++++ tests/components/mqtt/test_climate.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 5ac43d51316..2e19a345bc3 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -125,6 +125,8 @@ CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" CONF_TEMP_STEP = "temp_step" +PAYLOAD_NONE = "None" + MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( { climate.ATTR_AUX_HEAT, @@ -441,6 +443,12 @@ class MqttClimate(MqttEntity, ClimateEntity): if payload in CURRENT_HVAC_ACTIONS: self._action = payload self.async_write_ha_state() + elif not payload or payload == PAYLOAD_NONE: + _LOGGER.debug( + "Invalid %s action: %s, ignoring", + CURRENT_HVAC_ACTIONS, + payload, + ) else: _LOGGER.warning( "Invalid %s action: %s", diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 0f4f9b209c6..3b2da69f94b 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -900,6 +900,15 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hvac_action") == "cooling" + # Test ignoring null values + async_fire_mqtt_message(hass, "action", "null") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("hvac_action") == "cooling" + assert ( + "Invalid ['off', 'heating', 'cooling', 'drying', 'idle', 'fan'] action: None, ignoring" + in caplog.text + ) + async def test_set_with_templates(hass, mqtt_mock, caplog): """Test setting various attributes with templates.""" From 25ea728f21e9ea8730775349cacd83c89dd88adf Mon Sep 17 00:00:00 2001 From: Arjan van Balken Date: Thu, 27 Jan 2022 11:48:37 +0100 Subject: [PATCH 003/298] Update Arris TG2492LG dependency to 1.2.1 (#64999) --- homeassistant/components/arris_tg2492lg/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json index 8ed5c39882e..01da8b8af3c 100644 --- a/homeassistant/components/arris_tg2492lg/manifest.json +++ b/homeassistant/components/arris_tg2492lg/manifest.json @@ -2,7 +2,7 @@ "domain": "arris_tg2492lg", "name": "Arris TG2492LG", "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", - "requirements": ["arris-tg2492lg==1.1.0"], + "requirements": ["arris-tg2492lg==1.2.1"], "codeowners": ["@vanbalken"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b834a9fcb9d..2424b24a2c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -338,7 +338,7 @@ aqualogic==2.6 arcam-fmj==0.12.0 # homeassistant.components.arris_tg2492lg -arris-tg2492lg==1.1.0 +arris-tg2492lg==1.2.1 # homeassistant.components.ampio asmog==0.0.6 From a7d83993becd7c005e7b3b1fbe8b1052f7f03ac7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jan 2022 11:35:41 +1300 Subject: [PATCH 004/298] Add diagnostics download to ESPHome (#65008) --- .../components/esphome/diagnostics.py | 32 +++++++++++++++++ tests/components/esphome/conftest.py | 36 ++++++++++++++++++- tests/components/esphome/test_diagnostics.py | 26 ++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/esphome/diagnostics.py create mode 100644 tests/components/esphome/test_diagnostics.py diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py new file mode 100644 index 00000000000..4d9b769791c --- /dev/null +++ b/homeassistant/components/esphome/diagnostics.py @@ -0,0 +1,32 @@ +"""Diahgnostics support for ESPHome.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from . import CONF_NOISE_PSK, DomainData + +CONF_MAC_ADDRESS = "mac_address" + +REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, CONF_MAC_ADDRESS} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + diag: dict[str, Any] = {} + + diag["config"] = config_entry.as_dict() + + entry_data = DomainData.get(hass).get_entry_data(config_entry) + + if (storage_data := await entry_data.store.async_load()) is not None: + storage_data = cast("dict[str, Any]", storage_data) + diag["storage_data"] = storage_data + + return async_redact_data(diag, REDACT_KEYS) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 91368044723..7cf25f13015 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -1,8 +1,42 @@ """esphome session fixtures.""" - import pytest +from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + @pytest.fixture(autouse=True) def esphome_mock_async_zeroconf(mock_async_zeroconf): """Auto mock zeroconf.""" + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="ESPHome Device", + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.2", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "12345678123456781234567812345678", + }, + unique_id="esphome-device", + ) + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the ESPHome integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py new file mode 100644 index 00000000000..319bc2602e1 --- /dev/null +++ b/tests/components/esphome/test_diagnostics.py @@ -0,0 +1,26 @@ +"""Tests for the diagnostics data provided by the ESPHome integration.""" + +from aiohttp import ClientSession + +from homeassistant.components.esphome import CONF_NOISE_PSK +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry +): + """Test diagnostics for config entry.""" + result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + + assert isinstance(result, dict) + assert result["config"]["data"] == { + CONF_HOST: "192.168.1.2", + CONF_PORT: 6053, + CONF_PASSWORD: "**REDACTED**", + CONF_NOISE_PSK: "**REDACTED**", + } + assert result["config"]["unique_id"] == "esphome-device" From 9eb18564b7341eddbf6bb631e7d6aedaa8fef5ad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jan 2022 23:05:01 +0100 Subject: [PATCH 005/298] Handle Tuya sendings strings instead of numeric values (#65009) --- homeassistant/components/tuya/base.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index b51fa361121..ac9a9c83be2 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -74,7 +74,16 @@ class IntegerTypeData: @classmethod def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData: """Load JSON string and return a IntegerTypeData object.""" - return cls(dpcode, **json.loads(data)) + parsed = json.loads(data) + return cls( + dpcode, + min=int(parsed["min"]), + max=int(parsed["max"]), + scale=float(parsed["scale"]), + step=float(parsed["step"]), + unit=parsed.get("unit"), + type=parsed.get("type"), + ) @dataclass From c831270262dbec60832e17b17eb952874afa1883 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jan 2022 22:50:20 -0600 Subject: [PATCH 006/298] Bump flux_led to fix push updates on newer devices (#65011) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index f23daf5d053..ac324431ba6 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.11"], + "requirements": ["flux_led==0.28.17"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 2424b24a2c3..ce890037d60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.11 +flux_led==0.28.17 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c26847aacc..270da44c914 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.11 +flux_led==0.28.17 # homeassistant.components.homekit fnvhash==0.1.0 From 03e369dc86d154df1b6fcd25260709fc9cc9e903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 27 Jan 2022 10:03:45 +0100 Subject: [PATCH 007/298] Set ping data to None instead of False (#65013) --- homeassistant/components/ping/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 97e2aff7bff..ff88412a6ef 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -140,7 +140,7 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity): self._available = True if last_state is None or last_state.state != STATE_ON: - self._ping.data = False + self._ping.data = None return attributes = last_state.attributes From 057f1a701f46d3bf71ed1174dbc132620b1c21b0 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 26 Jan 2022 22:40:53 -0500 Subject: [PATCH 008/298] Bump pymazda to 0.3.2 (#65025) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 158bed0466d..e00049101f9 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.3.1"], + "requirements": ["pymazda==0.3.2"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index ce890037d60..1ad6372a6a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1660,7 +1660,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.1 +pymazda==0.3.2 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 270da44c914..1e929340cdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1041,7 +1041,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.1 +pymazda==0.3.2 # homeassistant.components.melcloud pymelcloud==2.5.6 From 662ec1377a85d06aeef2e89451b330cef7bf25c9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Jan 2022 02:02:27 -0800 Subject: [PATCH 009/298] Catch connection reset error (#65027) --- homeassistant/components/hassio/ingress.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 91aca485917..284ba42b3c1 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -154,7 +154,11 @@ class HassIOIngress(HomeAssistantView): async for data in result.content.iter_chunked(4096): await response.write(data) - except (aiohttp.ClientError, aiohttp.ClientPayloadError) as err: + except ( + aiohttp.ClientError, + aiohttp.ClientPayloadError, + ConnectionResetError, + ) as err: _LOGGER.debug("Stream error %s / %s: %s", token, path, err) return response From 290a0df2beff4fb7fd12cf7fa0a6de432b7c934c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 27 Jan 2022 10:43:23 -0600 Subject: [PATCH 010/298] Update rokuecp to 0.12.0 (#65030) --- homeassistant/components/roku/manifest.json | 2 +- homeassistant/components/roku/media_player.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roku/test_media_player.py | 14 +++++++------- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 7c712c81e1f..7dd5974589c 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.11.0"], + "requirements": ["rokuecp==0.12.0"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 4d064d5d326..67abae262d5 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -408,13 +408,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if attr in extra } - await self.coordinator.roku.play_video(media_id, params) + await self.coordinator.roku.play_on_roku(media_id, params) elif media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: params = { "MediaType": "hls", } - await self.coordinator.roku.play_video(media_id, params) + await self.coordinator.roku.play_on_roku(media_id, params) await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 1ad6372a6a5..58aea66c491 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2111,7 +2111,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.11.0 +rokuecp==0.12.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e929340cdd..b2785535014 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1294,7 +1294,7 @@ rflink==0.0.62 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.11.0 +rokuecp==0.12.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index d42e06aceb8..a039b313702 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -476,8 +476,8 @@ async def test_services( blocking=True, ) - assert mock_roku.play_video.call_count == 1 - mock_roku.play_video.assert_called_with( + assert mock_roku.play_on_roku.call_count == 1 + mock_roku.play_on_roku.assert_called_with( "https://awesome.tld/media.mp4", { "videoName": "Sent from HA", @@ -496,8 +496,8 @@ async def test_services( blocking=True, ) - assert mock_roku.play_video.call_count == 2 - mock_roku.play_video.assert_called_with( + assert mock_roku.play_on_roku.call_count == 2 + mock_roku.play_on_roku.assert_called_with( "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", { "MediaType": "hls", @@ -551,9 +551,9 @@ async def test_services_play_media_local_source( blocking=True, ) - assert mock_roku.play_video.call_count == 1 - assert mock_roku.play_video.call_args - call_args = mock_roku.play_video.call_args.args + assert mock_roku.play_on_roku.call_count == 1 + assert mock_roku.play_on_roku.call_args + call_args = mock_roku.play_on_roku.call_args.args assert "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] From 25ffda7cd4db16efb193e1a40b700db783580552 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 27 Jan 2022 12:00:38 +0200 Subject: [PATCH 011/298] Fix Shelly battery powered devices unavailable (#65031) --- homeassistant/components/shelly/entity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index a8546cb43dd..12f82016a1c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -562,6 +562,11 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self.block: Block | None = block # type: ignore[assignment] self.entity_description = description + self._attr_should_poll = False + self._attr_device_info = DeviceInfo( + connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + ) + if block is not None: self._attr_unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}" self._attr_name = get_block_entity_name( From a768de51c08a93064e05c8b1470af7f12480fb93 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jan 2022 19:01:09 +0100 Subject: [PATCH 012/298] Correct zone state (#65040) Co-authored-by: Franck Nijhof --- .../components/device_tracker/config_entry.py | 1 + homeassistant/components/zone/__init__.py | 35 ++++-- tests/components/zone/test_init.py | 103 ++++++++---------- 3 files changed, 72 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 096268c8fed..18d769df07f 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -224,6 +224,7 @@ class TrackerEntity(BaseTrackerEntity): """Return the device state attributes.""" attr: dict[str, StateType] = {} attr.update(super().state_attributes) + if self.latitude is not None and self.longitude is not None: attr[ATTR_LATITUDE] = self.latitude attr[ATTR_LONGITUDE] = self.longitude diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 21f7363695e..41fdd8c32d3 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( ATTR_EDITABLE, + ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ICON, @@ -22,14 +23,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) -from homeassistant.core import ( - Event, - HomeAssistant, - ServiceCall, - State, - callback, - split_entity_id, -) +from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.helpers import ( collection, config_validation as cv, @@ -346,10 +340,20 @@ class Zone(entity.Entity): @callback def _person_state_change_listener(self, evt: Event) -> None: - object_id = split_entity_id(self.entity_id)[1] person_entity_id = evt.data["entity_id"] cur_count = len(self._persons_in_zone) - if evt.data["new_state"] and evt.data["new_state"].state == object_id: + if ( + (state := evt.data["new_state"]) + and (latitude := state.attributes.get(ATTR_LATITUDE)) is not None + and (longitude := state.attributes.get(ATTR_LONGITUDE)) is not None + and (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is not None + and ( + zone_state := async_active_zone( + self.hass, latitude, longitude, accuracy + ) + ) + and zone_state.entity_id == self.entity_id + ): self._persons_in_zone.add(person_entity_id) elif person_entity_id in self._persons_in_zone: self._persons_in_zone.remove(person_entity_id) @@ -362,10 +366,17 @@ class Zone(entity.Entity): await super().async_added_to_hass() person_domain = "person" # avoid circular import persons = self.hass.states.async_entity_ids(person_domain) - object_id = split_entity_id(self.entity_id)[1] for person in persons: state = self.hass.states.get(person) - if state and state.state == object_id: + if ( + state is None + or (latitude := state.attributes.get(ATTR_LATITUDE)) is None + or (longitude := state.attributes.get(ATTR_LONGITUDE)) is None + or (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is None + ): + continue + zone_state = async_active_zone(self.hass, latitude, longitude, accuracy) + if zone_state is not None and zone_state.entity_id == self.entity_id: self._persons_in_zone.add(person) self.async_on_remove( diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 399afd480c7..54cb87aa772 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -512,7 +512,7 @@ async def test_state(hass): "latitude": 32.880837, "longitude": -117.237561, "radius": 250, - "passive": True, + "passive": False, } assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) @@ -521,28 +521,40 @@ async def test_state(hass): assert state.state == "0" # Person entity enters zone - hass.states.async_set("person.person1", "test_zone") + hass.states.async_set( + "person.person1", + "Test Zone", + {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, + ) await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "1" + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "0" # Person entity enters zone - hass.states.async_set("person.person2", "test_zone") + hass.states.async_set( + "person.person2", + "Test Zone", + {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, + ) await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "2" + assert hass.states.get("zone.test_zone").state == "2" + assert hass.states.get("zone.home").state == "0" # Person entity enters another zone - hass.states.async_set("person.person1", "home") + hass.states.async_set( + "person.person1", + "home", + {"latitude": 32.87336, "longitude": -117.22743, "gps_accuracy": 0}, + ) await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "1" + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "1" # Person entity removed hass.states.async_remove("person.person2") await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "0" + assert hass.states.get("zone.test_zone").state == "0" + assert hass.states.get("zone.home").state == "1" async def test_state_2(hass): @@ -555,7 +567,7 @@ async def test_state_2(hass): "latitude": 32.880837, "longitude": -117.237561, "radius": 250, - "passive": True, + "passive": False, } assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) @@ -564,56 +576,37 @@ async def test_state_2(hass): assert state.state == "0" # Person entity enters zone - hass.states.async_set("person.person1", "test_zone") + hass.states.async_set( + "person.person1", + "Test Zone", + {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, + ) await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "1" + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "0" # Person entity enters zone - hass.states.async_set("person.person2", "test_zone") + hass.states.async_set( + "person.person2", + "Test Zone", + {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, + ) await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "2" + assert hass.states.get("zone.test_zone").state == "2" + assert hass.states.get("zone.home").state == "0" # Person entity enters another zone - hass.states.async_set("person.person1", "home") + hass.states.async_set( + "person.person1", + "home", + {"latitude": 32.87336, "longitude": -117.22743, "gps_accuracy": 0}, + ) await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "1" + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "1" # Person entity removed hass.states.async_remove("person.person2") await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "0" - - -async def test_state_3(hass): - """Test the state of a zone.""" - hass.states.async_set("person.person1", "test_zone") - hass.states.async_set("person.person2", "test_zone") - - info = { - "name": "Test Zone", - "latitude": 32.880837, - "longitude": -117.237561, - "radius": 250, - "passive": True, - } - assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) - - assert len(hass.states.async_entity_ids("zone")) == 2 - state = hass.states.get("zone.test_zone") - assert state.state == "2" - - # Person entity enters another zone - hass.states.async_set("person.person1", "home") - await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "1" - - # Person entity removed - hass.states.async_remove("person.person2") - await hass.async_block_till_done() - state = hass.states.get("zone.test_zone") - assert state.state == "0" + assert hass.states.get("zone.test_zone").state == "0" + assert hass.states.get("zone.home").state == "1" From 3e94d39c648e528276cb60a2fbe5dbd7d2beba1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jan 2022 18:59:58 +0100 Subject: [PATCH 013/298] Unset Alexa authorized flag in additional case (#65044) --- homeassistant/components/cloud/alexa_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 8a845077dc9..56f49307662 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -192,10 +192,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): if self.should_report_state != self.is_reporting_states: if self.should_report_state: - with suppress( - alexa_errors.NoTokenAvailable, alexa_errors.RequireRelink - ): + try: await self.async_enable_proactive_mode() + except (alexa_errors.NoTokenAvailable, alexa_errors.RequireRelink): + await self.set_authorized(False) else: await self.async_disable_proactive_mode() From 035b589fcab501c36bee85234a083241be7d609c Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Thu, 27 Jan 2022 14:30:31 +0100 Subject: [PATCH 014/298] Add `flow_title` for HomeWizard Energy (#65047) --- homeassistant/components/homewizard/config_flow.py | 4 ++++ homeassistant/components/homewizard/strings.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 1b3211c4765..17f87680c62 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -127,6 +127,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._set_confirm_only() + self.context["title_placeholders"] = { + "name": f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})" + } + return self.async_show_form( step_id="discovery_confirm", description_placeholders={ diff --git a/homeassistant/components/homewizard/strings.json b/homeassistant/components/homewizard/strings.json index c3798b35678..b34c6906cc1 100644 --- a/homeassistant/components/homewizard/strings.json +++ b/homeassistant/components/homewizard/strings.json @@ -15,10 +15,10 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Detected unsupported API version", "api_not_enabled": "The API is not enabled. Enable API in the HomeWizard Energy App under settings", "device_not_supported": "This device is not supported", "unknown_error": "[%key:common::config_flow::error::unknown%]" } } -} \ No newline at end of file +} From 6f8b0a01b45542153f5535d47856e3074df6d8ae Mon Sep 17 00:00:00 2001 From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com> Date: Thu, 27 Jan 2022 19:02:10 +0100 Subject: [PATCH 015/298] Fix typo in entity name for launchlibrary (#65048) --- homeassistant/components/launch_library/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index b656f92fec7..d468c3a653f 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -85,7 +85,7 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = ( LaunchLibrarySensorEntityDescription( key="launch_probability", icon="mdi:dice-multiple", - name="Launch Probability", + name="Launch probability", native_unit_of_measurement=PERCENTAGE, value_fn=lambda nl: None if nl.probability == -1 else nl.probability, attributes_fn=lambda nl: None, From 8afb0aa44aa7c7b358ba3aaac49b9ec6f1bb38d1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 27 Jan 2022 17:37:40 +0100 Subject: [PATCH 016/298] Fix notify leaving zone blueprint (#65056) --- .../components/automation/blueprints/notify_leaving_zone.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 71abf8f865c..1dc8a0eddf8 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -34,7 +34,9 @@ variables: condition: condition: template - value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + # The first case handles leaving the Home zone which has a special state when zoning called 'home'. + # The second case handles leaving all other zones. + value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" action: - alias: "Notify that a person has left the zone" From 5a90f106d15528e189ef6e8dd49ccf657e9aaca5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 27 Jan 2022 18:59:27 +0100 Subject: [PATCH 017/298] Remove `backports.zoneinfo` dependency (#65069) --- homeassistant/package_constraints.txt | 1 - homeassistant/util/dt.py | 12 +++--------- requirements.txt | 1 - setup.py | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7759c2fe360..ff317e21396 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,6 @@ async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 awesomeversion==22.1.0 -backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 0c8a1cd9aad..4b4b798a2d8 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -5,16 +5,11 @@ import bisect from contextlib import suppress import datetime as dt import re -import sys -from typing import Any, cast +from typing import Any +import zoneinfo import ciso8601 -if sys.version_info[:2] >= (3, 9): - import zoneinfo -else: - from backports import zoneinfo - DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc @@ -48,8 +43,7 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: Async friendly. """ try: - # Cast can be removed when mypy is switched to Python 3.9. - return cast(dt.tzinfo, zoneinfo.ZoneInfo(time_zone_str)) + return zoneinfo.ZoneInfo(time_zone_str) except zoneinfo.ZoneInfoNotFoundError: return None diff --git a/requirements.txt b/requirements.txt index 54d4d1b1d19..c8ee1d91368 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 awesomeversion==22.1.0 -backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/setup.py b/setup.py index 26ad28428fa..efcf61b85fc 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ REQUIRES = [ "attrs==21.2.0", "atomicwrites==1.4.0", "awesomeversion==22.1.0", - 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2021.5.30", "ciso8601==2.2.0", From 1968ddb3fde4512e8c8eb87d771856cd88551a26 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 27 Jan 2022 19:41:50 +0100 Subject: [PATCH 018/298] Update PyVicare to 2.16.1 (#65073) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 98c9786f131..98a7eb4c07c 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.15.0"], + "requirements": ["PyViCare==2.16.1"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 58aea66c491..295d659f816 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -56,7 +56,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.5 # homeassistant.components.vicare -PyViCare==2.15.0 +PyViCare==2.16.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2785535014..3aa2af3c05a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -37,7 +37,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.5 # homeassistant.components.vicare -PyViCare==2.15.0 +PyViCare==2.16.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 07d2627dc5f9b120ec824a91085e80b65942128f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 Jan 2022 13:10:19 -0600 Subject: [PATCH 019/298] Guard browsing Spotify if setup failed (#65074) Co-authored-by: Franck Nijhof --- homeassistant/components/spotify/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 37f2d7f0057..5c36a0c71c3 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -4,6 +4,7 @@ import aiohttp from spotipy import Spotify, SpotifyException import voluptuous as vol +from homeassistant.components.media_player import BrowseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CREDENTIALS, @@ -60,7 +61,8 @@ async def async_browse_media( hass, media_content_type, media_content_id, *, can_play_artist=True ): """Browse Spotify media.""" - info = list(hass.data[DOMAIN].values())[0] + if not (info := next(iter(hass.data[DOMAIN].values()), None)): + raise BrowseError("No Spotify accounts available") return await async_browse_media_internal( hass, info[DATA_SPOTIFY_CLIENT], From 7e2d04ca776ac90292d9de61b7a47fd0933a8d03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Jan 2022 11:22:53 -0800 Subject: [PATCH 020/298] Bump frontend to 20220127.0 (#65075) --- 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 a29752188a2..1eef7aff083 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220126.0" + "home-assistant-frontend==20220127.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ff317e21396..44486677384 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.6.3 hass-nabucasa==0.52.0 -home-assistant-frontend==20220126.0 +home-assistant-frontend==20220127.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 295d659f816..42736f4d48f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220126.0 +home-assistant-frontend==20220127.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3aa2af3c05a..1a6139e3d0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220126.0 +home-assistant-frontend==20220127.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From ff445b69f428333893a5849cc655225fa10706ef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jan 2022 20:19:28 +0100 Subject: [PATCH 021/298] Update Renault to 0.1.7 (#65076) * Update Renault to 0.1.7 * Adjust tests accordingly Co-authored-by: epenet --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/const.py | 11 ++++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 118848ad6dd..9442ea8160b 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", "requirements": [ - "renault-api==0.1.4" + "renault-api==0.1.7" ], "codeowners": [ "@epenet" diff --git a/requirements_all.txt b/requirements_all.txt index 42736f4d48f..69f4a2de845 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2087,7 +2087,7 @@ raspyrfm-client==1.2.8 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.4 +renault-api==0.1.7 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a6139e3d0e..0100ee79044 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1282,7 +1282,7 @@ rachiopy==1.0.3 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.4 +renault-api==0.1.7 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index e1d7a3fc28c..91704a59b51 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -228,7 +228,7 @@ MOCK_VEHICLES = { }, "endpoints_available": [ True, # cockpit - False, # hvac-status + True, # hvac-status True, # location True, # battery-status True, # charge-mode @@ -237,6 +237,7 @@ MOCK_VEHICLES = { "battery_status": "battery_status_not_charging.json", "charge_mode": "charge_mode_schedule.json", "cockpit": "cockpit_ev.json", + "hvac_status": "hvac_status.json", "location": "location.json", }, Platform.BINARY_SENSOR: [ @@ -356,6 +357,14 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_ENTITY_ID: "sensor.reg_number_outside_temperature", + ATTR_STATE: "8.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, { ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ENTITY_ID: "sensor.reg_number_plug_state", From 0604185854d0a13ef90a1d5adf7f6ef4ef920d21 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Jan 2022 11:24:06 -0800 Subject: [PATCH 022/298] Bumped version to 2022.2.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acd93a3ccf4..e47ec7024cb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) From 2ff8f10b9f73cef35afff2adfa8e155e0eb2c6f9 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Fri, 28 Jan 2022 10:58:42 +0100 Subject: [PATCH 023/298] Check explicitly for None value in Overkiz integration (#65045) --- homeassistant/components/overkiz/cover_entities/awning.py | 3 ++- .../components/overkiz/cover_entities/generic_cover.py | 5 +++-- homeassistant/components/overkiz/light.py | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/overkiz/cover_entities/awning.py b/homeassistant/components/overkiz/cover_entities/awning.py index bb5b0e52186..bbce2c985ed 100644 --- a/homeassistant/components/overkiz/cover_entities/awning.py +++ b/homeassistant/components/overkiz/cover_entities/awning.py @@ -48,7 +48,8 @@ class Awning(OverkizGenericCover): None is unknown, 0 is closed, 100 is fully open. """ - if current_position := self.executor.select_state(OverkizState.CORE_DEPLOYMENT): + current_position = self.executor.select_state(OverkizState.CORE_DEPLOYMENT) + if current_position is not None: return cast(int, current_position) return None diff --git a/homeassistant/components/overkiz/cover_entities/generic_cover.py b/homeassistant/components/overkiz/cover_entities/generic_cover.py index 476ed23ae4d..60484620df1 100644 --- a/homeassistant/components/overkiz/cover_entities/generic_cover.py +++ b/homeassistant/components/overkiz/cover_entities/generic_cover.py @@ -51,9 +51,10 @@ class OverkizGenericCover(OverkizEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ - if position := self.executor.select_state( + position = self.executor.select_state( OverkizState.CORE_SLATS_ORIENTATION, OverkizState.CORE_SLATE_ORIENTATION - ): + ) + if position is not None: return 100 - cast(int, position) return None diff --git a/homeassistant/components/overkiz/light.py b/homeassistant/components/overkiz/light.py index 6075267b8e6..b640a184f58 100644 --- a/homeassistant/components/overkiz/light.py +++ b/homeassistant/components/overkiz/light.py @@ -79,8 +79,9 @@ class OverkizLight(OverkizEntity, LightEntity): @property def brightness(self) -> int | None: """Return the brightness of this light (0-255).""" - if brightness := self.executor.select_state(OverkizState.CORE_LIGHT_INTENSITY): - return round(cast(int, brightness) * 255 / 100) + value = self.executor.select_state(OverkizState.CORE_LIGHT_INTENSITY) + if value is not None: + return round(cast(int, value) * 255 / 100) return None From 05d7fef9f00499b653676af53883023854345635 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 27 Jan 2022 16:08:26 +0000 Subject: [PATCH 024/298] Better names for energy related homekit_controller sensors (#65055) --- .../components/homekit_controller/sensor.py | 14 +++++++------- .../specific_devices/test_connectsense.py | 16 ++++++++-------- .../specific_devices/test_eve_energy.py | 4 ++-- .../specific_devices/test_koogeek_p1eu.py | 4 ++-- .../specific_devices/test_koogeek_sw2.py | 4 ++-- .../specific_devices/test_vocolinc_vp3.py | 4 ++-- .../components/homekit_controller/test_sensor.py | 4 ++-- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index eb408834bca..0cec354e1ab 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -44,21 +44,21 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT, - name="Real Time Energy", + name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS, - name="Real Time Current", + name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20, - name="Real Time Current", + name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, @@ -72,7 +72,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { ), CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT, - name="Real Time Energy", + name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, @@ -100,14 +100,14 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { ), CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY, - name="Real Time Energy", + name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2, - name="Real Time Energy", + name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, @@ -121,7 +121,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { ), CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY, - name="Real Time Energy", + name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index 371ee360adc..2cbdf924319 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -35,16 +35,16 @@ async def test_connectsense_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.inwall_outlet_0394de_real_time_current", - friendly_name="InWall Outlet-0394DE Real Time Current", + entity_id="sensor.inwall_outlet_0394de_current", + friendly_name="InWall Outlet-0394DE Current", unique_id="homekit-1020301376-aid:1-sid:13-cid:18", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state="0.03", ), EntityTestInfo( - entity_id="sensor.inwall_outlet_0394de_real_time_energy", - friendly_name="InWall Outlet-0394DE Real Time Energy", + entity_id="sensor.inwall_outlet_0394de_power", + friendly_name="InWall Outlet-0394DE Power", unique_id="homekit-1020301376-aid:1-sid:13-cid:19", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=POWER_WATT, @@ -65,16 +65,16 @@ async def test_connectsense_setup(hass): state="on", ), EntityTestInfo( - entity_id="sensor.inwall_outlet_0394de_real_time_current_2", - friendly_name="InWall Outlet-0394DE Real Time Current", + entity_id="sensor.inwall_outlet_0394de_current_2", + friendly_name="InWall Outlet-0394DE Current", unique_id="homekit-1020301376-aid:1-sid:25-cid:30", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state="0.05", ), EntityTestInfo( - entity_id="sensor.inwall_outlet_0394de_real_time_energy_2", - friendly_name="InWall Outlet-0394DE Real Time Energy", + entity_id="sensor.inwall_outlet_0394de_power_2", + friendly_name="InWall Outlet-0394DE Power", unique_id="homekit-1020301376-aid:1-sid:25-cid:31", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=POWER_WATT, diff --git a/tests/components/homekit_controller/specific_devices/test_eve_energy.py b/tests/components/homekit_controller/specific_devices/test_eve_energy.py index e6cfa1eaa5e..0ba9b0bee25 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -59,9 +59,9 @@ async def test_eve_degree_setup(hass): state="0.400000005960464", ), EntityTestInfo( - entity_id="sensor.eve_energy_50ff_real_time_energy", + entity_id="sensor.eve_energy_50ff_power", unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:34", - friendly_name="Eve Energy 50FF Real Time Energy", + friendly_name="Eve Energy 50FF Power", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="0", diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index 78d3efb64bb..f93adc732ba 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -37,8 +37,8 @@ async def test_koogeek_p1eu_setup(hass): state="off", ), EntityTestInfo( - entity_id="sensor.koogeek_p1_a00aa0_real_time_energy", - friendly_name="Koogeek-P1-A00AA0 Real Time Energy", + entity_id="sensor.koogeek_p1_a00aa0_power", + friendly_name="Koogeek-P1-A00AA0 Power", unique_id="homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:22", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 7c7688be4ee..ed940cb6376 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -43,8 +43,8 @@ async def test_koogeek_sw2_setup(hass): state="off", ), EntityTestInfo( - entity_id="sensor.koogeek_sw2_187a91_real_time_energy", - friendly_name="Koogeek-SW2-187A91 Real Time Energy", + entity_id="sensor.koogeek_sw2_187a91_power", + friendly_name="Koogeek-SW2-187A91 Power", unique_id="homekit-CNNT061751001372-aid:1-sid:14-cid:18", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py index a082683cf21..da69b7fe309 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -37,8 +37,8 @@ async def test_vocolinc_vp3_setup(hass): state="on", ), EntityTestInfo( - entity_id="sensor.vocolinc_vp3_123456_real_time_energy", - friendly_name="VOCOlinc-VP3-123456 Real Time Energy", + entity_id="sensor.vocolinc_vp3_123456_power", + friendly_name="VOCOlinc-VP3-123456 Power", unique_id="homekit-EU0121203xxxxx07-aid:1-sid:48-cid:97", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index c50e23bac13..4c57d94b2b8 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -218,7 +218,7 @@ async def test_switch_with_sensor(hass, utcnow): # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. energy_helper = Helper( hass, - "sensor.testdevice_real_time_energy", + "sensor.testdevice_power", helper.pairing, helper.accessory, helper.config_entry, @@ -248,7 +248,7 @@ async def test_sensor_unavailable(hass, utcnow): # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. energy_helper = Helper( hass, - "sensor.testdevice_real_time_energy", + "sensor.testdevice_power", helper.pairing, helper.accessory, helper.config_entry, From 6f20a755830c029150ad52956b8c67850fa16609 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 28 Jan 2022 18:30:44 +0200 Subject: [PATCH 025/298] Fix Shelly detached switches automation triggers (#65059) --- homeassistant/components/shelly/utils.py | 15 +++-- .../components/shelly/test_device_trigger.py | 57 +++++++++++++------ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a824488327b..a01b5de133a 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -125,15 +125,22 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: return f"{entity_name} channel {chr(int(block.channel)+base)}" -def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool: +def is_block_momentary_input( + settings: dict[str, Any], block: Block, include_detached: bool = False +) -> bool: """Return true if block input button settings is set to a momentary type.""" + momentary_types = ["momentary", "momentary_on_release"] + + if include_detached: + momentary_types.append("detached") + # Shelly Button type is fixed to momentary and no btn_type if settings["device"]["type"] in SHBTN_MODELS: return True if settings.get("mode") == "roller": button_type = settings["rollers"][0]["button_type"] - return button_type in ["momentary", "momentary_on_release"] + return button_type in momentary_types button = settings.get("relays") or settings.get("lights") or settings.get("inputs") if button is None: @@ -148,7 +155,7 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool: channel = min(int(block.channel or 0), len(button) - 1) button_type = button[channel].get("btn_type") - return button_type in ["momentary", "momentary_on_release"] + return button_type in momentary_types def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime: @@ -171,7 +178,7 @@ def get_block_input_triggers( if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids: return [] - if not is_block_momentary_input(device.settings, block): + if not is_block_momentary_input(device.settings, block, True): return [] triggers = [] diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index aee171eb46e..0532fa5c82c 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -29,25 +29,48 @@ from tests.common import ( ) -async def test_get_triggers_block_device(hass, coap_wrapper): +@pytest.mark.parametrize( + "button_type, is_valid", + [ + ("momentary", True), + ("momentary_on_release", True), + ("detached", True), + ("toggle", False), + ], +) +async def test_get_triggers_block_device( + hass, coap_wrapper, monkeypatch, button_type, is_valid +): """Test we get the expected triggers from a shelly block device.""" assert coap_wrapper - expected_triggers = [ - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: coap_wrapper.device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: "single", - CONF_SUBTYPE: "button1", - }, - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: coap_wrapper.device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: "long", - CONF_SUBTYPE: "button1", - }, - ] + + monkeypatch.setitem( + coap_wrapper.device.settings, + "relays", + [ + {"btn_type": button_type}, + {"btn_type": "toggle"}, + ], + ) + + expected_triggers = [] + if is_valid: + expected_triggers = [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "long", + CONF_SUBTYPE: "button1", + }, + ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id From 74155133527883927d3e989c38a8efcaadc3d60e Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jan 2022 16:08:29 +0100 Subject: [PATCH 026/298] Add diagnostics support to P1 Monitor (#65060) * Add diagnostics to P1 Monitor * Add test for diagnostics --- .../components/p1_monitor/diagnostics.py | 35 +++++++++++ .../components/p1_monitor/test_diagnostics.py | 59 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 homeassistant/components/p1_monitor/diagnostics.py create mode 100644 tests/components/p1_monitor/test_diagnostics.py diff --git a/homeassistant/components/p1_monitor/diagnostics.py b/homeassistant/components/p1_monitor/diagnostics.py new file mode 100644 index 00000000000..627d0df767d --- /dev/null +++ b/homeassistant/components/p1_monitor/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for P1 Monitor.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from . import P1MonitorDataUpdateCoordinator +from .const import DOMAIN, SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER + +TO_REDACT = { + CONF_HOST, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: P1MonitorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + }, + "data": { + "smartmeter": coordinator.data[SERVICE_SMARTMETER].__dict__, + "phases": coordinator.data[SERVICE_PHASES].__dict__, + "settings": coordinator.data[SERVICE_SETTINGS].__dict__, + }, + } diff --git a/tests/components/p1_monitor/test_diagnostics.py b/tests/components/p1_monitor/test_diagnostics.py new file mode 100644 index 00000000000..6b97107c353 --- /dev/null +++ b/tests/components/p1_monitor/test_diagnostics.py @@ -0,0 +1,59 @@ +"""Tests for the diagnostics data provided by the P1 Monitor integration.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "monitor", + "data": { + "host": REDACTED, + }, + }, + "data": { + "smartmeter": { + "gas_consumption": 2273.447, + "energy_tariff_period": "high", + "power_consumption": 877, + "energy_consumption_high": 2770.133, + "energy_consumption_low": 4988.071, + "power_production": 0, + "energy_production_high": 3971.604, + "energy_production_low": 1432.279, + }, + "phases": { + "voltage_phase_l1": "233.6", + "voltage_phase_l2": "0.0", + "voltage_phase_l3": "233.0", + "current_phase_l1": "1.6", + "current_phase_l2": "4.44", + "current_phase_l3": "3.51", + "power_consumed_phase_l1": 315, + "power_consumed_phase_l2": 0, + "power_consumed_phase_l3": 624, + "power_produced_phase_l1": 0, + "power_produced_phase_l2": 0, + "power_produced_phase_l3": 0, + }, + "settings": { + "gas_consumption_price": "0.64", + "energy_consumption_price_high": "0.20522", + "energy_consumption_price_low": "0.20522", + "energy_production_price_high": "0.20522", + "energy_production_price_low": "0.20522", + }, + }, + } From 735edd83fc1a0dbc0289090e9c6ba9b12ac97be5 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 27 Jan 2022 22:02:30 +0000 Subject: [PATCH 027/298] Support unpairing homekit accessories from homekit_controller (#65065) --- .../components/homekit_controller/__init__.py | 20 +++++++++++++++ .../homekit_controller/test_init.py | 24 ++++++++++++++++++ .../homekit_controller/test_storage.py | 25 ------------------- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index a26978f537a..0f231fa9303 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import logging from typing import Any import aiohomekit @@ -26,6 +27,8 @@ from .connection import HKDevice, valid_serial_number from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage +_LOGGER = logging.getLogger(__name__) + def escape_characteristic_name(char_name): """Escape any dash or dots in a characteristics name.""" @@ -248,4 +251,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Cleanup caches before removing config entry.""" hkid = entry.data["AccessoryPairingID"] + + # Remove cached type data from .storage/homekit_controller-entity-map hass.data[ENTITY_MAP].async_delete_map(hkid) + + # Remove the pairing on the device, making the device discoverable again. + # Don't reuse any objects in hass.data as they are already unloaded + async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + controller = aiohomekit.Controller(async_zeroconf_instance=async_zeroconf_instance) + controller.load_pairing(hkid, dict(entry.data)) + try: + await controller.remove_pairing(hkid) + except aiohomekit.AccessoryDisconnectedError: + _LOGGER.warning( + "Accessory %s was removed from HomeAssistant but was not reachable " + "to properly unpair. It may need resetting before you can use it with " + "HomeKit again", + entry.title, + ) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index cd5662d73c9..d1b133468d5 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -4,8 +4,11 @@ from unittest.mock import patch from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from aiohomekit.testing import FakeController +from homeassistant.components.homekit_controller.const import ENTITY_MAP from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from tests.components.homekit_controller.common import setup_test_component @@ -27,3 +30,24 @@ async def test_unload_on_stop(hass, utcnow): await hass.async_block_till_done() assert async_unlock_mock.called + + +async def test_async_remove_entry(hass: HomeAssistant): + """Test unpairing a component.""" + helper = await setup_test_component(hass, create_motion_sensor_service) + + hkid = "00:00:00:00:00:00" + + with patch("aiohomekit.Controller") as controller_cls: + # Setup a fake controller with 1 pairing + controller = controller_cls.return_value = FakeController() + await controller.add_paired_device([helper.accessory], hkid) + assert len(controller.pairings) == 1 + + assert hkid in hass.data[ENTITY_MAP].storage_data + + # Remove it via config entry and number of pairings should go down + await helper.config_entry.async_remove(hass) + assert len(controller.pairings) == 0 + + assert hkid not in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index aa0a5e55057..b4ed617f901 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -2,8 +2,6 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant import config_entries -from homeassistant.components.homekit_controller import async_remove_entry from homeassistant.components.homekit_controller.const import ENTITY_MAP from tests.common import flush_store @@ -79,26 +77,3 @@ async def test_storage_is_updated_on_add(hass, hass_storage, utcnow): # Is saved out to store? await flush_store(entity_map.store) assert hkid in hass_storage[ENTITY_MAP]["data"]["pairings"] - - -async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): - """Test entity map storage is cleaned up on config entry removal.""" - await setup_test_component(hass, create_lightbulb_service) - - hkid = "00:00:00:00:00:00" - - pairing_data = {"AccessoryPairingID": hkid} - - entry = config_entries.ConfigEntry( - 1, - "homekit_controller", - "TestData", - pairing_data, - "test", - ) - - assert hkid in hass.data[ENTITY_MAP].storage_data - - await async_remove_entry(hass, entry) - - assert hkid not in hass.data[ENTITY_MAP].storage_data From 837d49f67b76e8f3bb4810bc29591e12eba6b6a7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jan 2022 21:01:30 +0100 Subject: [PATCH 028/298] Fix Yale optionsflow (#65072) --- .../components/yale_smart_alarm/config_flow.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index 8994d0b2fbd..1567f22be44 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -161,7 +161,10 @@ class YaleOptionsFlowHandler(OptionsFlow): errors = {} if user_input: - if len(user_input[CONF_CODE]) not in [0, user_input[CONF_LOCK_CODE_DIGITS]]: + if len(user_input.get(CONF_CODE, "")) not in [ + 0, + user_input[CONF_LOCK_CODE_DIGITS], + ]: errors["base"] = "code_format_mismatch" else: return self.async_create_entry(title="", data=user_input) @@ -171,7 +174,10 @@ class YaleOptionsFlowHandler(OptionsFlow): data_schema=vol.Schema( { vol.Optional( - CONF_CODE, default=self.entry.options.get(CONF_CODE) + CONF_CODE, + description={ + "suggested_value": self.entry.options.get(CONF_CODE) + }, ): str, vol.Optional( CONF_LOCK_CODE_DIGITS, From 3f763ddc9aba06451cfd931c5646a65bf549eeff Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 28 Jan 2022 17:33:31 +0100 Subject: [PATCH 029/298] Reconnect client service tried to connect even if device didn't exist (#65082) --- homeassistant/components/unifi/services.py | 3 ++ tests/components/unifi/test_services.py | 38 +++++++++------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index c0498395a10..fcde48528b3 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -57,6 +57,9 @@ async def async_reconnect_client(hass, data) -> None: device_registry = dr.async_get(hass) device_entry = device_registry.async_get(data[ATTR_DEVICE_ID]) + if device_entry is None: + return + mac = "" for connection in device_entry.connections: if connection[0] == CONNECTION_NETWORK_MAC: diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index b483e789f96..27e4ddea930 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -77,15 +77,26 @@ async def test_reconnect_client(hass, aioclient_mock): assert aioclient_mock.call_count == 1 +async def test_reconnect_non_existant_device(hass, aioclient_mock): + """Verify no call is made if device does not exist.""" + await setup_unifi_integration(hass, aioclient_mock) + + aioclient_mock.clear_requests() + + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: "device_entry.id"}, + blocking=True, + ) + assert aioclient_mock.call_count == 0 + + async def test_reconnect_device_without_mac(hass, aioclient_mock): """Verify no call is made if device does not have a known mac.""" config_entry = await setup_unifi_integration(hass, aioclient_mock) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", - ) device_registry = await hass.helpers.device_registry.async_get_registry() device_entry = device_registry.async_get_or_create( @@ -139,12 +150,8 @@ async def test_reconnect_client_controller_unavailable(hass, aioclient_mock): async def test_reconnect_client_unknown_mac(hass, aioclient_mock): """Verify no call is made if trying to reconnect a mac unknown to controller.""" config_entry = await setup_unifi_integration(hass, aioclient_mock) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", - ) device_registry = await hass.helpers.device_registry.async_get_registry() device_entry = device_registry.async_get_or_create( @@ -172,12 +179,8 @@ async def test_reconnect_wired_client(hass, aioclient_mock): config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=clients ) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", - ) device_registry = await hass.helpers.device_registry.async_get_registry() device_entry = device_registry.async_get_or_create( @@ -264,9 +267,6 @@ async def test_remove_clients_controller_unavailable(hass, aioclient_mock): controller.available = False aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", - ) await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) assert aioclient_mock.call_count == 0 @@ -281,15 +281,9 @@ async def test_remove_clients_no_call_on_empty_list(hass, aioclient_mock): "mac": "00:00:00:00:00:01", } ] - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_all_response=clients - ) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + await setup_unifi_integration(hass, aioclient_mock, clients_all_response=clients) aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", - ) await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) assert aioclient_mock.call_count == 0 From 909b0fb689d7ea0efc9fcf2f855d260bd62151ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jan 2022 08:16:28 -0800 Subject: [PATCH 030/298] Add support for proxy-selected intent (#65094) --- .../components/google_assistant/smart_home.py | 9 ++++++ .../google_assistant/test_smart_home.py | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 430169ea97d..31fd02544ff 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -294,6 +294,15 @@ async def async_devices_reachable(hass, data: RequestData, payload): } +@HANDLERS.register("action.devices.PROXY_SELECTED") +async def async_devices_proxy_selected(hass, data: RequestData, payload): + """Handle action.devices.PROXY_SELECTED request. + + When selected for local SDK. + """ + return {} + + def turned_off_response(message): """Return a device turned off response.""" return { diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 5ec43b37550..3398fdca926 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1514,3 +1514,34 @@ async def test_query_recover(hass, caplog): } }, } + + +async def test_proxy_selected(hass, caplog): + """Test that we handle proxy selected.""" + + result = await sh.async_handle_message( + hass, + BASIC_CONFIG, + "test-agent", + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.PROXY_SELECTED", + "payload": { + "device": { + "id": "abcdefg", + "customData": {}, + }, + "structureData": {}, + }, + } + ], + }, + const.SOURCE_LOCAL, + ) + + assert result == { + "requestId": REQ_ID, + "payload": {}, + } From 44403dab62115f128fe9d94ac42ea8dc0d602ced Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 28 Jan 2022 08:33:12 +0200 Subject: [PATCH 031/298] Fix Shelly 1/1PM external temperature sensor unavailable (#65096) --- homeassistant/components/shelly/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index efb7d3a3579..ce9c57f5889 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -223,7 +223,7 @@ SENSORS: Final = { device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.extTemp) != 999 - and not block.sensorError, + and not getattr(block, "sensorError", False), ), ("sensor", "humidity"): BlockSensorDescription( key="sensor|humidity", @@ -233,7 +233,7 @@ SENSORS: Final = { device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.humidity) != 999 - and not block.sensorError, + and not getattr(block, "sensorError", False), ), ("sensor", "luminosity"): BlockSensorDescription( key="sensor|luminosity", From 34cf82b017ddfd413d3309e443a561a0dab06a87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jan 2022 02:38:13 -0600 Subject: [PATCH 032/298] Downgrade homekit linked humidity sensor error to debug (#65098) Fixes #65015 --- homeassistant/components/homekit/type_humidifiers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 42115132420..09cfc02dcce 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -190,7 +190,7 @@ class HumidifierDehumidifier(HomeAccessory): ) self.char_current_humidity.set_value(current_humidity) except ValueError as ex: - _LOGGER.error( + _LOGGER.debug( "%s: Unable to update from linked humidity sensor %s: %s", self.entity_id, self.linked_humidity_sensor, From 0a00177a8f35440abbb5861fcd012106880e4323 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Fri, 28 Jan 2022 12:06:05 +0100 Subject: [PATCH 033/298] Handle vicare I/O in executor (#65105) Co-authored-by: Martin Hjelmare --- homeassistant/components/vicare/climate.py | 41 ++++++++++-------- .../components/vicare/water_heater.py | 42 +++++++++++-------- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index a528fd63614..451ea70edab 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -101,6 +101,15 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type): return ViCareClimate(name, vicare_api, device_config, circuit, heating_type) +def _get_circuits(vicare_api): + """Return the list of circuits.""" + try: + return vicare_api.circuits + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No circuits found") + return [] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -108,25 +117,23 @@ async def async_setup_entry( ) -> None: """Set up the ViCare climate platform.""" name = VICARE_NAME - entities = [] + api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] + circuits = await hass.async_add_executor_job(_get_circuits, api) - try: - for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits: - suffix = "" - if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits) > 1: - suffix = f" {circuit.id}" - entity = _build_entity( - f"{name} Heating{suffix}", - hass.data[DOMAIN][config_entry.entry_id][VICARE_API], - hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], - circuit, - config_entry.data[CONF_HEATING_TYPE], - ) - if entity is not None: - entities.append(entity) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("No circuits found") + for circuit in circuits: + suffix = "" + if len(circuits) > 1: + suffix = f" {circuit.id}" + + entity = _build_entity( + f"{name} Heating{suffix}", + api, + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], + circuit, + config_entry.data[CONF_HEATING_TYPE], + ) + entities.append(entity) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 5912287a953..0107ff8fe4c 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -68,6 +68,15 @@ def _build_entity(name, vicare_api, circuit, device_config, heating_type): ) +def _get_circuits(vicare_api): + """Return the list of circuits.""" + try: + return vicare_api.circuits + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No circuits found") + return [] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -75,24 +84,23 @@ async def async_setup_entry( ) -> None: """Set up the ViCare climate platform.""" name = VICARE_NAME - entities = [] - try: - for circuit in hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits: - suffix = "" - if len(hass.data[DOMAIN][config_entry.entry_id][VICARE_API].circuits) > 1: - suffix = f" {circuit.id}" - entity = _build_entity( - f"{name} Water{suffix}", - hass.data[DOMAIN][config_entry.entry_id][VICARE_API], - circuit, - hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], - config_entry.data[CONF_HEATING_TYPE], - ) - if entity is not None: - entities.append(entity) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("No circuits found") + api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] + circuits = await hass.async_add_executor_job(_get_circuits, api) + + for circuit in circuits: + suffix = "" + if len(circuits) > 1: + suffix = f" {circuit.id}" + + entity = _build_entity( + f"{name} Water{suffix}", + api, + circuit, + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], + config_entry.data[CONF_HEATING_TYPE], + ) + entities.append(entity) async_add_entities(entities) From 82acaa380cfee6e515282c851d4c3e0454b11274 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 Jan 2022 11:38:09 +0100 Subject: [PATCH 034/298] Fix cast support for browsing local media source (#65115) --- homeassistant/components/cast/media_player.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index f96279d6d7f..e966e98c3f1 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -471,9 +471,16 @@ class CastDevice(MediaPlayerEntity): "audio/" ) - if plex.is_plex_media_id(media_content_id): - return await plex.async_browse_media( - self.hass, media_content_type, media_content_id, platform=CAST_DOMAIN + if media_content_id is not None: + if plex.is_plex_media_id(media_content_id): + return await plex.async_browse_media( + self.hass, + media_content_type, + media_content_id, + platform=CAST_DOMAIN, + ) + return await media_source.async_browse_media( + self.hass, media_content_id, **kwargs ) if media_content_type == "plex": From d382e24e5b9abded59037b0777bfe5c96c28df7a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 28 Jan 2022 15:54:19 +0100 Subject: [PATCH 035/298] Goodwe - fix value errors (#65121) --- homeassistant/components/goodwe/number.py | 2 +- homeassistant/components/goodwe/select.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 614cab3d90a..06a31a4e10a 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -76,7 +76,7 @@ async def async_setup_entry( for description in NUMBERS: try: current_value = await description.getter(inverter) - except InverterError: + except (InverterError, ValueError): # Inverter model does not support this setting _LOGGER.debug("Could not read inverter setting %s", description.key) continue diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index b8ff5c91a3c..985c799110d 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -42,7 +42,7 @@ async def async_setup_entry( # read current operating mode from the inverter try: active_mode = await inverter.get_operation_mode() - except InverterError: + except (InverterError, ValueError): # Inverter model does not support this setting _LOGGER.debug("Could not read inverter operation mode") else: From 0f9e65e687ce046b1ee71be9d9853ffeb61dc6d4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 28 Jan 2022 17:32:46 +0100 Subject: [PATCH 036/298] Handle FritzInternalError exception for Fritz (#65124) --- homeassistant/components/fritz/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 37dfdc0e554..b86a435e758 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -14,6 +14,7 @@ from fritzconnection.core.exceptions import ( FritzActionError, FritzActionFailedError, FritzConnectionException, + FritzInternalError, FritzLookUpError, FritzSecurityError, FritzServiceError, @@ -523,6 +524,7 @@ class AvmWrapper(FritzBoxTools): except ( FritzActionError, FritzActionFailedError, + FritzInternalError, FritzServiceError, FritzLookUpError, ): From 1e60958fc45718d161ea4ab923ba1ec809519d31 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jan 2022 18:00:47 +0100 Subject: [PATCH 037/298] Add diagnostics support to onewire (#65131) * Add diagnostics support to onewire * Add tests Co-authored-by: epenet --- .../components/onewire/diagnostics.py | 33 ++++++++++ tests/components/onewire/test_diagnostics.py | 61 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 homeassistant/components/onewire/diagnostics.py create mode 100644 tests/components/onewire/test_diagnostics.py diff --git a/homeassistant/components/onewire/diagnostics.py b/homeassistant/components/onewire/diagnostics.py new file mode 100644 index 00000000000..a02ff2d8e47 --- /dev/null +++ b/homeassistant/components/onewire/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics support for 1-Wire.""" +from __future__ import annotations + +from dataclasses import asdict +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .onewirehub import OneWireHub + +TO_REDACT = {CONF_HOST} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + onewirehub: OneWireHub = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + "options": {**entry.options}, + }, + "devices": [asdict(device_details) for device_details in onewirehub.devices] + if onewirehub.devices + else [], + } diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py new file mode 100644 index 00000000000..bc164a9b138 --- /dev/null +++ b/tests/components/onewire/test_diagnostics.py @@ -0,0 +1,61 @@ +"""Test 1-Wire diagnostics.""" +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_owproxy_mock_devices + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): + yield + + +DEVICE_DETAILS = { + "device_info": { + "identifiers": [["onewire", "EF.111111111113"]], + "manufacturer": "Hobby Boards", + "model": "HB_HUB", + "name": "EF.111111111113", + }, + "family": "EF", + "id": "EF.111111111113", + "path": "/EF.111111111113/", + "type": "HB_HUB", +} + + +@pytest.mark.parametrize("device_id", ["EF.111111111113"], indirect=True) +async def test_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, + hass_client, + owproxy: MagicMock, + device_id: str, +): + """Test config entry diagnostics.""" + setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "data": { + "host": REDACTED, + "port": 1234, + "type": "OWServer", + }, + "options": {}, + "title": "Mock Title", + }, + "devices": [DEVICE_DETAILS], + } From 6ba52b1c86df13f1040792e5d045bb92eabea91f Mon Sep 17 00:00:00 2001 From: Nenad Bogojevic Date: Fri, 28 Jan 2022 17:48:16 +0100 Subject: [PATCH 038/298] Use new withings oauth2 refresh token endpoint (#65134) --- homeassistant/components/withings/__init__.py | 4 +- homeassistant/components/withings/common.py | 43 +++++++++++++++++++ homeassistant/components/withings/sensor.py | 1 - tests/components/withings/common.py | 14 +++--- tests/components/withings/test_config_flow.py | 14 +++--- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 800f8b654bb..701694e40e9 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -9,7 +9,7 @@ import asyncio from aiohttp.web import Request, Response import voluptuous as vol -from withings_api import WithingsAuth +from withings_api import AbstractWithingsApi, WithingsAuth from withings_api.common import NotifyAppli from homeassistant.components import webhook @@ -84,7 +84,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], f"{WithingsAuth.URL}/oauth2_user/authorize2", - f"{WithingsAuth.URL}/oauth2/token", + f"{AbstractWithingsApi.URL}/v2/oauth2", ), ) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 8da67a0b77a..56f7f7cdf91 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1111,3 +1111,46 @@ class WithingsLocalOAuth2Implementation(LocalOAuth2Implementation): """Return the redirect uri.""" url = get_url(self.hass, allow_internal=False, prefer_cloud=True) return f"{url}{AUTH_CALLBACK_PATH}" + + async def _token_request(self, data: dict) -> dict: + """Make a token request and adapt Withings API reply.""" + new_token = await super()._token_request(data) + # Withings API returns habitual token data under json key "body": + # { + # "status": [{integer} Withings API response status], + # "body": { + # "access_token": [{string} Your new access_token], + # "expires_in": [{integer} Access token expiry delay in seconds], + # "token_type": [{string] HTTP Authorization Header format: Bearer], + # "scope": [{string} Scopes the user accepted], + # "refresh_token": [{string} Your new refresh_token], + # "userid": [{string} The Withings ID of the user] + # } + # } + # so we copy that to token root. + if body := new_token.pop("body", None): + new_token.update(body) + return new_token + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve the authorization code to tokens.""" + return await self._token_request( + { + "action": "requesttoken", + "grant_type": "authorization_code", + "code": external_data["code"], + "redirect_uri": external_data["state"]["redirect_uri"], + } + ) + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh tokens.""" + new_token = await self._token_request( + { + "action": "requesttoken", + "grant_type": "refresh_token", + "client_id": self.client_id, + "refresh_token": token["refresh_token"], + } + ) + return {**token, **new_token} diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 0ca40d28440..f8753739519 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -15,7 +15,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" - entities = await async_create_entities( hass, entry, diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 71eb350410b..b90a004ed0b 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -216,13 +216,15 @@ class ComponentFactory: self._aioclient_mock.clear_requests() self._aioclient_mock.post( - "https://account.withings.com/oauth2/token", + "https://wbsapi.withings.net/v2/oauth2", json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - "userid": profile_config.user_id, + "body": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "userid": profile_config.user_id, + }, }, ) diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 210d1f669e9..2643ac18c24 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -90,13 +90,15 @@ async def test_config_reauth_profile( aioclient_mock.clear_requests() aioclient_mock.post( - "https://account.withings.com/oauth2/token", + "https://wbsapi.withings.net/v2/oauth2", json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - "userid": "0", + "body": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "userid": "0", + }, }, ) From 6c3e8b06eae2291b82b57273dcbbbc075cc0af7d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 28 Jan 2022 08:09:08 -0800 Subject: [PATCH 039/298] Bump google-nest-sdm to 1.6.0 (diagnostics) (#65135) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 9c686362d98..478e608700c 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==1.5.1"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==1.6.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 69f4a2de845..3a3ae91fe2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==1.5.1 +google-nest-sdm==1.6.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0100ee79044..83086ebb786 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.9.0 # homeassistant.components.nest -google-nest-sdm==1.5.1 +google-nest-sdm==1.6.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From fdb52df7b725775a3376de6219c13bf8a3e1f9a0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 28 Jan 2022 09:07:41 -0800 Subject: [PATCH 040/298] Add diagnostics for rtsp_to_webrtc (#65138) --- .../components/rtsp_to_webrtc/diagnostics.py | 17 +++ tests/components/rtsp_to_webrtc/conftest.py | 98 +++++++++++++++ .../rtsp_to_webrtc/test_diagnostics.py | 27 ++++ tests/components/rtsp_to_webrtc/test_init.py | 118 ++++-------------- 4 files changed, 168 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/rtsp_to_webrtc/diagnostics.py create mode 100644 tests/components/rtsp_to_webrtc/conftest.py create mode 100644 tests/components/rtsp_to_webrtc/test_diagnostics.py diff --git a/homeassistant/components/rtsp_to_webrtc/diagnostics.py b/homeassistant/components/rtsp_to_webrtc/diagnostics.py new file mode 100644 index 00000000000..ab13e0a64ee --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/diagnostics.py @@ -0,0 +1,17 @@ +"""Diagnostics support for Nest.""" + +from __future__ import annotations + +from typing import Any + +from rtsp_to_webrtc import client + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return dict(client.get_diagnostics()) diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py new file mode 100644 index 00000000000..7148896e454 --- /dev/null +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -0,0 +1,98 @@ +"""Tests for RTSPtoWebRTC inititalization.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator, Awaitable, Callable, Generator +from typing import Any, TypeVar +from unittest.mock import patch + +import pytest +import rtsp_to_webrtc + +from homeassistant.components import camera +from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +STREAM_SOURCE = "rtsp://example.com" +SERVER_URL = "http://127.0.0.1:8083" + +CONFIG_ENTRY_DATA = {"server_url": SERVER_URL} + +# Typing helpers +ComponentSetup = Callable[[], Awaitable[None]] +T = TypeVar("T") +YieldFixture = Generator[T, None, None] + + +@pytest.fixture(autouse=True) +async def webrtc_server() -> None: + """Patch client library to force usage of RTSPtoWebRTC server.""" + with patch( + "rtsp_to_webrtc.client.WebClient.heartbeat", + side_effect=rtsp_to_webrtc.exceptions.ResponseError(), + ): + yield + + +@pytest.fixture +async def mock_camera(hass) -> AsyncGenerator[None, None]: + """Initialize a demo camera platform.""" + assert await async_setup_component( + hass, "camera", {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + with patch( + "homeassistant.components.demo.camera.Path.read_bytes", + return_value=b"Test", + ), patch( + "homeassistant.components.camera.Camera.stream_source", + return_value=STREAM_SOURCE, + ), patch( + "homeassistant.components.camera.Camera.supported_features", + return_value=camera.SUPPORT_STREAM, + ): + yield + + +@pytest.fixture +async def config_entry_data() -> dict[str, Any]: + """Fixture for MockConfigEntry data.""" + return CONFIG_ENTRY_DATA + + +@pytest.fixture +async def config_entry(config_entry_data: dict[str, Any]) -> MockConfigEntry: + """Fixture for MockConfigEntry.""" + return MockConfigEntry(domain=DOMAIN, data=config_entry_data) + + +@pytest.fixture +async def rtsp_to_webrtc_client() -> None: + """Fixture for mock rtsp_to_webrtc client.""" + with patch("rtsp_to_webrtc.client.Client.heartbeat"): + yield + + +@pytest.fixture +async def setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> YieldFixture[ComponentSetup]: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + async def func() -> None: + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + yield func + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert entries[0].state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/rtsp_to_webrtc/test_diagnostics.py b/tests/components/rtsp_to_webrtc/test_diagnostics.py new file mode 100644 index 00000000000..27b801a71ed --- /dev/null +++ b/tests/components/rtsp_to_webrtc/test_diagnostics.py @@ -0,0 +1,27 @@ +"""Test nest diagnostics.""" + +from typing import Any + +from .conftest import ComponentSetup + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + +THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT" + + +async def test_entry_diagnostics( + hass, + hass_client, + config_entry: MockConfigEntry, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, +): + """Test config entry diagnostics.""" + await setup_integration() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "discovery": {"attempt": 1, "web.failure": 1, "webrtc.success": 1}, + "web": {}, + "webrtc": {}, + } diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py index 94ec3529836..759fea7c813 100644 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ b/tests/components/rtsp_to_webrtc/test_init.py @@ -3,7 +3,7 @@ from __future__ import annotations import base64 -from collections.abc import AsyncGenerator, Awaitable, Callable +from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import patch @@ -11,147 +11,84 @@ import aiohttp import pytest import rtsp_to_webrtc -from homeassistant.components import camera from homeassistant.components.rtsp_to_webrtc import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup + from tests.test_util.aiohttp import AiohttpClientMocker -STREAM_SOURCE = "rtsp://example.com" # The webrtc component does not inspect the details of the offer and answer, # and is only a pass through. OFFER_SDP = "v=0\r\no=carol 28908764872 28908764872 IN IP4 100.3.6.6\r\n..." ANSWER_SDP = "v=0\r\no=bob 2890844730 2890844730 IN IP4 host.example.com\r\n..." -SERVER_URL = "http://127.0.0.1:8083" -CONFIG_ENTRY_DATA = {"server_url": SERVER_URL} - - -@pytest.fixture(autouse=True) -async def webrtc_server() -> None: - """Patch client library to force usage of RTSPtoWebRTC server.""" - with patch( - "rtsp_to_webrtc.client.WebClient.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ResponseError(), - ): - yield - - -@pytest.fixture -async def mock_camera(hass) -> AsyncGenerator[None, None]: - """Initialize a demo camera platform.""" - assert await async_setup_component( - hass, "camera", {camera.DOMAIN: {"platform": "demo"}} - ) - await hass.async_block_till_done() - with patch( - "homeassistant.components.demo.camera.Path.read_bytes", - return_value=b"Test", - ), patch( - "homeassistant.components.camera.Camera.stream_source", - return_value=STREAM_SOURCE, - ), patch( - "homeassistant.components.camera.Camera.supported_features", - return_value=camera.SUPPORT_STREAM, - ): - yield - - -async def async_setup_rtsp_to_webrtc(hass: HomeAssistant) -> None: - """Set up the component.""" - return await async_setup_component(hass, DOMAIN, {}) - - -async def test_setup_success(hass: HomeAssistant) -> None: +async def test_setup_success( + hass: HomeAssistant, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup +) -> None: """Test successful setup and unload.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) - - with patch("rtsp_to_webrtc.client.Client.heartbeat"): - assert await async_setup_rtsp_to_webrtc(hass) - await hass.async_block_till_done() + await setup_integration() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state is ConfigEntryState.LOADED - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert not hass.data.get(DOMAIN) - assert config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_invalid_config_entry(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("config_entry_data", [{}]) +async def test_invalid_config_entry( + hass: HomeAssistant, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup +) -> None: """Test a config entry with missing required fields.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}) - config_entry.add_to_hass(hass) - - assert await async_setup_rtsp_to_webrtc(hass) + await setup_integration() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state is ConfigEntryState.SETUP_ERROR -async def test_setup_server_failure(hass: HomeAssistant) -> None: +async def test_setup_server_failure( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: """Test server responds with a failure on startup.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) - with patch( "rtsp_to_webrtc.client.Client.heartbeat", side_effect=rtsp_to_webrtc.exceptions.ResponseError(), ): - assert await async_setup_rtsp_to_webrtc(hass) - await hass.async_block_till_done() + await setup_integration() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state is ConfigEntryState.SETUP_RETRY - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - -async def test_setup_communication_failure(hass: HomeAssistant) -> None: +async def test_setup_communication_failure( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: """Test unable to talk to server on startup.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) - with patch( "rtsp_to_webrtc.client.Client.heartbeat", side_effect=rtsp_to_webrtc.exceptions.ClientError(), ): - assert await async_setup_rtsp_to_webrtc(hass) - await hass.async_block_till_done() + await setup_integration() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state is ConfigEntryState.SETUP_RETRY - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - async def test_offer_for_stream_source( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], mock_camera: Any, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, ) -> None: """Test successful response from RTSPtoWebRTC server.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) - - with patch("rtsp_to_webrtc.client.Client.heartbeat"): - assert await async_setup_rtsp_to_webrtc(hass) - await hass.async_block_till_done() + await setup_integration() aioclient_mock.post( f"{SERVER_URL}/stream", @@ -188,14 +125,11 @@ async def test_offer_failure( aioclient_mock: AiohttpClientMocker, hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], mock_camera: Any, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, ) -> None: """Test a transient failure talking to RTSPtoWebRTC server.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) - - with patch("rtsp_to_webrtc.client.Client.heartbeat"): - assert await async_setup_rtsp_to_webrtc(hass) - await hass.async_block_till_done() + await setup_integration() aioclient_mock.post( f"{SERVER_URL}/stream", From 8e38b7624e7e9e85a3b33617ccb65f1a2b3c4f19 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jan 2022 09:37:50 -0800 Subject: [PATCH 041/298] Bumped version to 2022.2.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e47ec7024cb..c2101246b53 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) From 4eb787b61963bda49c8770b1ff20ef432358a95e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 28 Jan 2022 07:34:18 +0100 Subject: [PATCH 042/298] Move `install_requires` to `setup.cfg` (#65095) --- .pre-commit-config.yaml | 2 +- script/gen_requirements_all.py | 8 ++++---- setup.cfg | 28 ++++++++++++++++++++++++++++ setup.py | 29 ----------------------------- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17575ebe375..256a0d7d155 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -107,7 +107,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/manifest\.json|setup\.py|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ + files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ce2178288a0..872f2d0c7a8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Generate an updated requirements_all.txt.""" +import configparser import difflib import importlib import os @@ -167,10 +168,9 @@ def explore_module(package, explore_children): def core_requirements(): """Gather core requirements out of setup.py.""" - reqs_raw = re.search( - r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S - ).group(1) - return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] + parser = configparser.ConfigParser() + parser.read("setup.cfg") + return parser["options"]["install_requires"].strip().split("\n") def gather_recursive_requirements(domain, seen=None): diff --git a/setup.cfg b/setup.cfg index f285902985c..4b226b4402c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,34 @@ classifier = Programming Language :: Python :: 3.9 Topic :: Home Automation +[options] +install_requires = + aiohttp==3.8.1 + astral==2.2 + async_timeout==4.0.2 + attrs==21.2.0 + atomicwrites==1.4.0 + awesomeversion==22.1.0 + bcrypt==3.1.7 + certifi>=2021.5.30 + ciso8601==2.2.0 + # When bumping httpx, please check the version pins of + # httpcore, anyio, and h11 in gen_requirements_all + httpx==0.21.3 + ifaddr==0.1.7 + jinja2==3.0.3 + PyJWT==2.1.0 + # PyJWT has loose dependency. We want the latest one. + cryptography==35.0.0 + pip>=8.0.3,<20.3 + python-slugify==4.0.1 + pyyaml==6.0 + requests==2.27.1 + typing-extensions>=3.10.0.2,<5.0 + voluptuous==0.12.2 + voluptuous-serialize==2.5.0 + yarl==1.7.2 + [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build max-complexity = 25 diff --git a/setup.py b/setup.py index efcf61b85fc..c6f3f1fb02f 100755 --- a/setup.py +++ b/setup.py @@ -31,34 +31,6 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) -REQUIRES = [ - "aiohttp==3.8.1", - "astral==2.2", - "async_timeout==4.0.2", - "attrs==21.2.0", - "atomicwrites==1.4.0", - "awesomeversion==22.1.0", - "bcrypt==3.1.7", - "certifi>=2021.5.30", - "ciso8601==2.2.0", - # When bumping httpx, please check the version pins of - # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.21.3", - "ifaddr==0.1.7", - "jinja2==3.0.3", - "PyJWT==2.1.0", - # PyJWT has loose dependency. We want the latest one. - "cryptography==35.0.0", - "pip>=8.0.3,<20.3", - "python-slugify==4.0.1", - "pyyaml==6.0", - "requests==2.27.1", - "typing-extensions>=3.10.0.2,<5.0", - "voluptuous==0.12.2", - "voluptuous-serialize==2.5.0", - "yarl==1.7.2", -] - MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) setup( @@ -72,7 +44,6 @@ setup( packages=PACKAGES, include_package_data=True, zip_safe=False, - install_requires=REQUIRES, python_requires=f">={MIN_PY_VERSION}", test_suite="tests", entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]}, From 931884386773e818d435f280a7ec85d4c34e0a5f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 28 Jan 2022 13:36:20 +0100 Subject: [PATCH 043/298] Move version metadata key to setup.cfg (#65091) * Move version to setup.cfg * Move python_requires to setup.cfg * Add script to validate project metadata * Add dedicated pre-commit hook --- .pre-commit-config.yaml | 7 +++++++ script/hassfest/__main__.py | 2 ++ script/hassfest/metadata.py | 31 +++++++++++++++++++++++++++++++ script/version_bump.py | 14 +++++++++++++- setup.cfg | 2 ++ setup.py | 4 ---- 6 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 script/hassfest/metadata.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 256a0d7d155..87c7d9e9102 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -115,3 +115,10 @@ repos: language: script types: [text] files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|\.strict-typing|homeassistant/.+/services\.yaml|script/hassfest/.+\.py)$ + - id: hassfest-metadata + name: hassfest-metadata + entry: script/run-in-env.sh python3 -m script.hassfest -p metadata + pass_filenames: false + language: script + types: [text] + files: ^(script/hassfest/.+\.py|homeassistant/const\.py$|setup\.cfg)$ diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index d4935196cc7..ac3d3ce8a85 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -12,6 +12,7 @@ from . import ( dhcp, json, manifest, + metadata, mqtt, mypy_config, requirements, @@ -41,6 +42,7 @@ INTEGRATION_PLUGINS = [ HASS_PLUGINS = [ coverage, mypy_config, + metadata, ] diff --git a/script/hassfest/metadata.py b/script/hassfest/metadata.py new file mode 100644 index 00000000000..ab5ba3f036d --- /dev/null +++ b/script/hassfest/metadata.py @@ -0,0 +1,31 @@ +"""Package metadata validation.""" +import configparser + +from homeassistant.const import REQUIRED_PYTHON_VER, __version__ + +from .model import Config, Integration + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate project metadata keys.""" + metadata_path = config.root / "setup.cfg" + parser = configparser.ConfigParser() + parser.read(metadata_path) + + try: + if parser["metadata"]["version"] != __version__: + config.add_error( + "metadata", f"'metadata.version' value does not match '{__version__}'" + ) + except KeyError: + config.add_error("metadata", "No 'metadata.version' key found!") + + required_py_version = f">={'.'.join(map(str, REQUIRED_PYTHON_VER))}" + try: + if parser["options"]["python_requires"] != required_py_version: + config.add_error( + "metadata", + f"'options.python_requires' value doesn't match '{required_py_version}", + ) + except KeyError: + config.add_error("metadata", "No 'options.python_requires' key found!") diff --git a/script/version_bump.py b/script/version_bump.py index 5f1988f3c26..6044cdb277c 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -117,7 +117,18 @@ def write_version(version): ) with open("homeassistant/const.py", "wt") as fil: - content = fil.write(content) + fil.write(content) + + +def write_version_metadata(version: Version) -> None: + """Update setup.cfg file with new version.""" + with open("setup.cfg") as fp: + content = fp.read() + + content = re.sub(r"(version\W+=\W).+\n", f"\\g<1>{version}\n", content, count=1) + + with open("setup.cfg", "w") as fp: + fp.write(content) def main(): @@ -142,6 +153,7 @@ def main(): assert bumped > current, "BUG! New version is not newer than old version" write_version(bumped) + write_version_metadata(bumped) if not arguments.commit: return diff --git a/setup.cfg b/setup.cfg index 4b226b4402c..0625178d78f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,5 @@ [metadata] +version = 2022.3.0.dev0 license = Apache-2.0 license_file = LICENSE.md platforms = any @@ -15,6 +16,7 @@ classifier = Topic :: Home Automation [options] +python_requires = >=3.9.0 install_requires = aiohttp==3.8.1 astral==2.2 diff --git a/setup.py b/setup.py index c6f3f1fb02f..403ba9f0a33 100755 --- a/setup.py +++ b/setup.py @@ -31,11 +31,8 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) -MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) - setup( name=PROJECT_PACKAGE_NAME, - version=hass_const.__version__, url=PROJECT_URL, download_url=DOWNLOAD_URL, project_urls=PROJECT_URLS, @@ -44,7 +41,6 @@ setup( packages=PACKAGES, include_package_data=True, zip_safe=False, - python_requires=f">={MIN_PY_VERSION}", test_suite="tests", entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]}, ) From 3829a81d158062cab8543f3a0d3bdfff5dd449bf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 28 Jan 2022 17:11:46 +0100 Subject: [PATCH 044/298] Move `project_urls` to `setup.cfg` (#65129) --- setup.cfg | 7 +++++++ setup.py | 21 --------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0625178d78f..274e1ac362a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,13 @@ platforms = any description = Open-source home automation platform running on Python 3. long_description = file: README.rst keywords = home, automation +url = https://www.home-assistant.io/ +project_urls = + Source Code = https://github.com/home-assistant/core + Bug Reports = https://github.com/home-assistant/core/issues + Docs: Dev = https://developers.home-assistant.io/ + Discord = https://discordapp.com/invite/c5DvZ4e + Forum = https://community.home-assistant.io/ classifier = Development Status :: 4 - Beta Intended Audience :: End Users/Desktop diff --git a/setup.py b/setup.py index 403ba9f0a33..febaab62be0 100755 --- a/setup.py +++ b/setup.py @@ -4,38 +4,17 @@ from datetime import datetime as dt from setuptools import find_packages, setup -import homeassistant.const as hass_const - PROJECT_NAME = "Home Assistant" PROJECT_PACKAGE_NAME = "homeassistant" PROJECT_LICENSE = "Apache License 2.0" PROJECT_AUTHOR = "The Home Assistant Authors" PROJECT_COPYRIGHT = f" 2013-{dt.now().year}, {PROJECT_AUTHOR}" -PROJECT_URL = "https://www.home-assistant.io/" PROJECT_EMAIL = "hello@home-assistant.io" -PROJECT_GITHUB_USERNAME = "home-assistant" -PROJECT_GITHUB_REPOSITORY = "core" - -PYPI_URL = f"https://pypi.python.org/pypi/{PROJECT_PACKAGE_NAME}" -GITHUB_PATH = f"{PROJECT_GITHUB_USERNAME}/{PROJECT_GITHUB_REPOSITORY}" -GITHUB_URL = f"https://github.com/{GITHUB_PATH}" - -DOWNLOAD_URL = f"{GITHUB_URL}/archive/{hass_const.__version__}.zip" -PROJECT_URLS = { - "Bug Reports": f"{GITHUB_URL}/issues", - "Dev Docs": "https://developers.home-assistant.io/", - "Discord": "https://discordapp.com/invite/c5DvZ4e", - "Forum": "https://community.home-assistant.io/", -} - PACKAGES = find_packages(exclude=["tests", "tests.*"]) setup( name=PROJECT_PACKAGE_NAME, - url=PROJECT_URL, - download_url=DOWNLOAD_URL, - project_urls=PROJECT_URLS, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, packages=PACKAGES, From 25e6d8858c4c7f2eecf884ffccf871b62a193480 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 28 Jan 2022 12:50:38 -0800 Subject: [PATCH 045/298] Update nest diagnostics (#65141) --- homeassistant/components/nest/diagnostics.py | 17 +++++------------ tests/components/nest/conftest.py | 9 +++++++++ tests/components/nest/test_diagnostics.py | 20 ++++++++++++++------ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index b60889358fd..0b6cfff6bae 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any +from google_nest_sdm import diagnostics from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait from google_nest_sdm.exceptions import ApiException @@ -30,22 +31,14 @@ async def async_get_config_entry_diagnostics( return {"error": str(err)} return { + **diagnostics.get_diagnostics(), "devices": [ get_device_data(device) for device in device_manager.devices.values() - ] + ], } def get_device_data(device: Device) -> dict[str, Any]: """Return diagnostic information about a device.""" - # Return a simplified view of the API object, but skipping any id fields or - # traits that include unique identifiers or personally identifiable information. - # See https://developers.google.com/nest/device-access/traits for API details - return { - "type": device.type, - "traits": { - trait: data - for trait, data in device.raw_data.get("traits", {}).items() - if trait not in REDACT_DEVICE_TRAITS - }, - } + # Library performs its own redaction for device data + return device.get_diagnostics() diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index b13dbf662b9..9b060d38fbe 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -1,6 +1,7 @@ """Common libraries for test setup.""" from __future__ import annotations +from collections.abc import Generator import copy import shutil from typing import Any @@ -8,6 +9,7 @@ from unittest.mock import patch import uuid import aiohttp +from google_nest_sdm import diagnostics from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device_manager import DeviceManager import pytest @@ -234,3 +236,10 @@ async def setup_platform( ) -> PlatformSetup: """Fixture to setup the integration platform and subscriber.""" return setup_base_platform + + +@pytest.fixture(autouse=True) +def reset_diagnostics() -> Generator[None, None, None]: + """Fixture to reset client library diagnostic counters.""" + yield + diagnostics.reset() diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index 09930c18501..b603019da81 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -56,13 +56,21 @@ async def test_entry_diagnostics(hass, hass_client): assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "devices": [ { - "traits": { - "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1 + "data": { + "assignee": "**REDACTED**", + "name": "**REDACTED**", + "parentRelations": [ + {"displayName": "**REDACTED**", "parent": "**REDACTED**"} + ], + "traits": { + "sdm.devices.traits.Info": {"customName": "**REDACTED**"}, + "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1 + }, }, - }, - "type": "sdm.devices.types.THERMOSTAT", + "type": "sdm.devices.types.THERMOSTAT", + } } ], } From 421f9716a79a8c465db9b245b30d19250256540a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 28 Jan 2022 23:09:05 +0100 Subject: [PATCH 046/298] Use isolated build environments (#65145) --- .github/workflows/builder.yml | 6 ++++-- pyproject.toml | 4 ++++ script/release | 32 -------------------------------- 3 files changed, 8 insertions(+), 34 deletions(-) delete mode 100755 script/release diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 89c4d02c942..74016d4492c 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -76,8 +76,10 @@ jobs: - name: Build package shell: bash run: | - pip install twine wheel - python setup.py sdist bdist_wheel + # Remove dist, build, and homeassistant.egg-info + # when build locally for testing! + pip install twine build + python -m build - name: Upload package shell: bash diff --git a/pyproject.toml b/pyproject.toml index 52b000bd1af..69398645d18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools~=60.5", "wheel~=0.37.1"] +build-backend = "setuptools.build_meta" + [tool.black] target-version = ["py38"] exclude = 'generated' diff --git a/script/release b/script/release deleted file mode 100755 index 4dc94eb7f15..00000000000 --- a/script/release +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -# Pushes a new version to PyPi. - -cd "$(dirname "$0")/.." - -head -n 5 homeassistant/const.py | tail -n 1 | grep PATCH_VERSION > /dev/null - -if [ $? -eq 1 ] -then - echo "Patch version not found on const.py line 5" - exit 1 -fi - -head -n 5 homeassistant/const.py | tail -n 1 | grep dev > /dev/null - -if [ $? -eq 0 ] -then - echo "Release version should not contain dev tag" - exit 1 -fi - -CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` - -if [ "$CURRENT_BRANCH" != "master" ] && [ "$CURRENT_BRANCH" != "rc" ] -then - echo "You have to be on the master or rc branch to release." - exit 1 -fi - -rm -rf dist build -python3 setup.py sdist bdist_wheel -python3 -m twine upload dist/* --skip-existing From 4ead2f2f7eab9ea7b538c836e79ffb45a93ca7b8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 28 Jan 2022 22:57:12 +0100 Subject: [PATCH 047/298] Fix excepton for SamsungTV getting device info (#65151) --- homeassistant/components/samsungtv/bridge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 262bf4ce67f..d509da91304 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod import contextlib from typing import Any +from requests.exceptions import Timeout as RequestsTimeout from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse from samsungtvws import SamsungTVWS @@ -321,7 +322,7 @@ class SamsungTVWSBridge(SamsungTVBridge): def device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" if remote := self._get_remote(avoid_open=True): - with contextlib.suppress(HttpApiError): + with contextlib.suppress(HttpApiError, RequestsTimeout): device_info: dict[str, Any] = remote.rest_device_info() return device_info From 84f817eb258cf8acfa0e5fb9a0fd76a82d20ac60 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 29 Jan 2022 06:14:51 +0100 Subject: [PATCH 048/298] Fix status for Fritz device tracker (#65152) --- homeassistant/components/fritz/common.py | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index b86a435e758..9d1c4543857 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -343,14 +343,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): for interf in node["node_interfaces"]: dev_mac = interf["mac_address"] + + if dev_mac not in hosts: + continue + + dev_info: Device = hosts[dev_mac] + for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) - if ( - intf is not None - and link["state"] == "CONNECTED" - and dev_mac in hosts - ): - dev_info: Device = hosts[dev_mac] + if intf is not None: if intf["op_mode"] != "AP_GUEST": dev_info.wan_access = not self.connection.call_action( "X_AVM-DE_HostFilter:1", @@ -361,14 +362,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): dev_info.connected_to = intf["device"] dev_info.connection_type = intf["type"] dev_info.ssid = intf.get("ssid") + _LOGGER.debug("Client dev_info: %s", dev_info) - if dev_mac in self._devices: - self._devices[dev_mac].update(dev_info, consider_home) - else: - device = FritzDevice(dev_mac, dev_info.name) - device.update(dev_info, consider_home) - self._devices[dev_mac] = device - new_device = True + if dev_mac in self._devices: + self._devices[dev_mac].update(dev_info, consider_home) + else: + device = FritzDevice(dev_mac, dev_info.name) + device.update(dev_info, consider_home) + self._devices[dev_mac] = device + new_device = True dispatcher_send(self.hass, self.signal_device_update) if new_device: From 2bfedcbdc538047474e7ce37e06da5fa5cd8272f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 29 Jan 2022 05:18:09 +0100 Subject: [PATCH 049/298] Move remaining keys to `setup.cfg` (#65154) * Move metadata keys * Move options * Delete setup.py * Remove unused constants * Remove deprecated test_suite key * Improve metadata * Only include homeassistant*, not script* * Add long_desc_content_type * Remove license file (auto-included by setuptools + wheels) * Add setup.py Pip 21.2 doesn't support editable installs without it. --- MANIFEST.in | 1 - setup.cfg | 16 +++++++++++++++- setup.py | 30 ++++++------------------------ 3 files changed, 21 insertions(+), 26 deletions(-) mode change 100755 => 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in index 490b550e705..780ffd02719 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ include README.rst -include LICENSE.md graft homeassistant recursive-exclude * *.py[co] diff --git a/setup.cfg b/setup.cfg index 274e1ac362a..061e0bbc0cb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,13 @@ [metadata] +name = homeassistant version = 2022.3.0.dev0 +author = The Home Assistant Authors +author_email = hello@home-assistant.io license = Apache-2.0 -license_file = LICENSE.md platforms = any description = Open-source home automation platform running on Python 3. long_description = file: README.rst +long_description_content_type = text/x-rst keywords = home, automation url = https://www.home-assistant.io/ project_urls = @@ -23,6 +26,9 @@ classifier = Topic :: Home Automation [options] +packages = find: +zip_safe = False +include_package_data = True python_requires = >=3.9.0 install_requires = aiohttp==3.8.1 @@ -51,6 +57,14 @@ install_requires = voluptuous-serialize==2.5.0 yarl==1.7.2 +[options.packages.find] +include = + homeassistant* + +[options.entry_points] +console_scripts = + hass = homeassistant.__main__:main + [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build max-complexity = 25 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index febaab62be0..69bf65dd8a4 --- a/setup.py +++ b/setup.py @@ -1,25 +1,7 @@ -#!/usr/bin/env python3 -"""Home Assistant setup script.""" -from datetime import datetime as dt +""" +Entry point for setuptools. Required for editable installs. +TODO: Remove file after updating to pip 21.3 +""" +from setuptools import setup -from setuptools import find_packages, setup - -PROJECT_NAME = "Home Assistant" -PROJECT_PACKAGE_NAME = "homeassistant" -PROJECT_LICENSE = "Apache License 2.0" -PROJECT_AUTHOR = "The Home Assistant Authors" -PROJECT_COPYRIGHT = f" 2013-{dt.now().year}, {PROJECT_AUTHOR}" -PROJECT_EMAIL = "hello@home-assistant.io" - -PACKAGES = find_packages(exclude=["tests", "tests.*"]) - -setup( - name=PROJECT_PACKAGE_NAME, - author=PROJECT_AUTHOR, - author_email=PROJECT_EMAIL, - packages=PACKAGES, - include_package_data=True, - zip_safe=False, - test_suite="tests", - entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]}, -) +setup() From 406801ef73201b6c4ab10cda7acd221b7130c64c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Jan 2022 06:05:53 +0100 Subject: [PATCH 050/298] Fix setting speed of Tuya fan (#65155) --- homeassistant/components/tuya/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 53de4f392b8..42849d4498d 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -137,7 +137,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): [ { "code": self._speed.dpcode, - "value": self._speed.scale_value_back(percentage), + "value": int(self._speed.remap_value_from(percentage, 0, 100)), } ] ) From c74a8bf65a7b7e8f3c75742eed94246237d58f3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jan 2022 23:13:28 -0600 Subject: [PATCH 051/298] Add OUI for KL430 tplink light strip to discovery (#65159) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 1531f96c545..2da05abc35e 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -25,6 +25,10 @@ "hostname": "k[lp]*", "macaddress": "403F8C*" }, + { + "hostname": "k[lp]*", + "macaddress": "C0C9E3*" + }, { "hostname": "ep*", "macaddress": "E848B8*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 6c9e0d87499..8875fb15b5b 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -392,6 +392,11 @@ DHCP = [ "hostname": "k[lp]*", "macaddress": "403F8C*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "C0C9E3*" + }, { "domain": "tplink", "hostname": "ep*", From ca505b79b5ce3864da5bc6f26a415d6921a1ebb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jan 2022 23:13:41 -0600 Subject: [PATCH 052/298] Add dhcp discovery to oncue (#65160) --- homeassistant/components/oncue/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index 1b3548296ee..cc450ada61a 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -2,6 +2,10 @@ "domain": "oncue", "name": "Oncue by Kohler", "config_flow": true, + "dhcp": [{ + "hostname": "kohlergen*", + "macaddress": "00146F*" + }], "documentation": "https://www.home-assistant.io/integrations/oncue", "requirements": ["aiooncue==0.3.2"], "codeowners": ["@bdraco"], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8875fb15b5b..277bc6169e9 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -201,6 +201,11 @@ DHCP = [ "domain": "nuki", "hostname": "nuki_bridge_*" }, + { + "domain": "oncue", + "hostname": "kohlergen*", + "macaddress": "00146F*" + }, { "domain": "overkiz", "hostname": "gateway*", From fb3c99a891220bd92e68e87610b5c28f6b3b6146 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jan 2022 23:13:54 -0600 Subject: [PATCH 053/298] Add additional roomba OUIs to DHCP discovery (#65161) --- homeassistant/components/roomba/manifest.json | 6 +++++- homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index ad5857aa630..6313c800dea 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -13,7 +13,11 @@ { "hostname": "roomba-*", "macaddress": "80A589*" - } + }, + { + "hostname": "roomba-*", + "macaddress": "DCF505*" + } ], "iot_class": "local_push" } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 277bc6169e9..0c7538f7266 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -255,6 +255,11 @@ DHCP = [ "hostname": "roomba-*", "macaddress": "80A589*" }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "DCF505*" + }, { "domain": "samsungtv", "hostname": "tizen*" From 5f561071160dc6b87e2548b7dd586f8aa269b7e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jan 2022 23:14:08 -0600 Subject: [PATCH 054/298] Add additional blink OUIs to DHCP discovery (#65162) --- homeassistant/components/blink/manifest.json | 6 +++++- homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index b90e7e845cf..cc185061ee9 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -8,7 +8,11 @@ { "hostname": "blink*", "macaddress": "B85F98*" - } + }, + { + "hostname": "blink*", + "macaddress": "00037F*" + } ], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 0c7538f7266..f05a7f73e50 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -46,6 +46,11 @@ DHCP = [ "hostname": "blink*", "macaddress": "B85F98*" }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "00037F*" + }, { "domain": "broadlink", "macaddress": "34EA34*" From f8e0c41e916256e59016c7bf9b6a74e2da1124c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jan 2022 23:14:30 -0600 Subject: [PATCH 055/298] Fix uncaught exception during isy994 dhcp discovery with ignored entry (#65165) --- .../components/isy994/config_flow.py | 2 ++ tests/components/isy994/test_config_flow.py | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 7289f7d416e..4e700df24cb 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -158,6 +158,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(isy_mac) if not existing_entry: return + if existing_entry.source == config_entries.SOURCE_IGNORE: + raise data_entry_flow.AbortFlow("already_configured") parsed_url = urlparse(existing_entry.data[CONF_HOST]) if parsed_url.hostname != ip_address: new_netloc = ip_address diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index e9a4c5dc4fb..b16f5c0070d 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -16,7 +16,12 @@ from homeassistant.components.isy994.const import ( ISY_URL_POSTFIX, UDN_UUID_PREFIX, ) -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IMPORT, SOURCE_SSDP +from homeassistant.config_entries import ( + SOURCE_DHCP, + SOURCE_IGNORE, + SOURCE_IMPORT, + SOURCE_SSDP, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -595,3 +600,27 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" assert entry.data[CONF_USERNAME] == "bob" + + +async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant): + """Test we handled an ignored entry from dhcp.""" + + entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MOCK_UUID, source=SOURCE_IGNORE + ) + entry.add_to_hass(hass) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="isy994-ems", + macaddress=MOCK_MAC, + ), + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From cd6c182c07dd57d22a69904df29a33f8b17e4ac9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jan 2022 21:53:21 -0800 Subject: [PATCH 056/298] Bumped version to 2022.2.0b3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c2101246b53..6f0f4011810 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 061e0bbc0cb..e0ec1326e20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0.dev0 +version = 2022.2.0b3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 3dde12f8876b15a94be7dd9fb45bc3322f390b53 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 27 Jan 2022 23:03:20 +0100 Subject: [PATCH 057/298] Add tests for KNX diagnostic and expose (#64938) * Add test for KNX diagnostic * Add test for KNX expose * Apply review suggestions --- .coveragerc | 5 -- homeassistant/components/knx/expose.py | 2 - tests/components/knx/conftest.py | 7 ++- tests/components/knx/test_diagnostic.py | 67 +++++++++++++++++++++++++ tests/components/knx/test_expose.py | 30 ++++++++++- 5 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 tests/components/knx/test_diagnostic.py diff --git a/.coveragerc b/.coveragerc index c301d4e30d3..f0af68a9cda 100644 --- a/.coveragerc +++ b/.coveragerc @@ -560,12 +560,7 @@ omit = homeassistant/components/knx/__init__.py homeassistant/components/knx/climate.py homeassistant/components/knx/cover.py - homeassistant/components/knx/diagnostics.py - homeassistant/components/knx/expose.py - homeassistant/components/knx/knx_entity.py - homeassistant/components/knx/light.py homeassistant/components/knx/notify.py - homeassistant/components/knx/schema.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/const.py diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 6fa5a3ba728..34949b8c0b8 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -148,8 +148,6 @@ class KNXExposeSensor: async def _async_set_knx_value(self, value: StateType) -> None: """Set new value on xknx ExposeSensor.""" - if value is None: - return await self.device.set(value) diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 71a86f1e397..cd15d77629c 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -13,7 +13,7 @@ from xknx.telegram import Telegram, TelegramDirection from xknx.telegram.address import GroupAddress, IndividualAddress from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite -from homeassistant.components.knx import ConnectionSchema +from homeassistant.components.knx import ConnectionSchema, KNXModule from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, @@ -40,6 +40,11 @@ class KNXTestKit: # telegrams to an InternalGroupAddress won't be queued here self._outgoing_telegrams: asyncio.Queue = asyncio.Queue() + @property + def knx_module(self) -> KNXModule: + """Get the KNX module.""" + return self.hass.data[KNX_DOMAIN] + def assert_state(self, entity_id: str, state: str, **attributes) -> None: """Assert the state of an entity.""" test_state = self.hass.states.get(entity_id) diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py new file mode 100644 index 00000000000..697ee45ac07 --- /dev/null +++ b/tests/components/knx/test_diagnostic.py @@ -0,0 +1,67 @@ +"""Tests for the diagnostics data provided by the KNX integration.""" +from unittest.mock import patch + +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.knx.conftest import KNXTestKit + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + mock_config_entry: MockConfigEntry, + knx: KNXTestKit, +): + """Test diagnostics.""" + await knx.setup_integration({}) + + with patch("homeassistant.config.async_hass_config_yaml", return_value={}): + # Overwrite the version for this test since we don't want to change this with every library bump + knx.xknx.version = "1.0.0" + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == { + "config_entry_data": { + "connection_type": "automatic", + "individual_address": "15.15.250", + "multicast_group": "224.0.23.12", + "multicast_port": 3671, + }, + "configuration_error": None, + "configuration_yaml": None, + "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, + } + + +async def test_diagnostic_config_error( + hass: HomeAssistant, + hass_client: ClientSession, + mock_config_entry: MockConfigEntry, + knx: KNXTestKit, +): + """Test diagnostics.""" + await knx.setup_integration({}) + + with patch( + "homeassistant.config.async_hass_config_yaml", + return_value={"knx": {"wrong_key": {}}}, + ): + # Overwrite the version for this test since we don't want to change this with every library bump + knx.xknx.version = "1.0.0" + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == { + "config_entry_data": { + "connection_type": "automatic", + "individual_address": "15.15.250", + "multicast_group": "224.0.23.12", + "multicast_port": 3671, + }, + "configuration_error": "extra keys not allowed @ data['knx']['wrong_key']", + "configuration_yaml": {"wrong_key": {}}, + "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, + } diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 25ec0f92604..cbe174127c4 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -1,5 +1,8 @@ """Test KNX expose.""" -from homeassistant.components.knx import CONF_KNX_EXPOSE, KNX_ADDRESS +import time +from unittest.mock import patch + +from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS from homeassistant.components.knx.schema import ExposeSchema from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_TYPE from homeassistant.core import HomeAssistant @@ -123,3 +126,28 @@ async def test_expose_attribute_with_default(hass: HomeAssistant, knx: KNXTestKi # Change state to "off"; no attribute hass.states.async_set(entity_id, "off", {}) await knx.assert_write("1/1/8", (0,)) + + +@patch("time.localtime") +async def test_expose_with_date(localtime, hass: HomeAssistant, knx: KNXTestKit): + """Test an expose with a date.""" + localtime.return_value = time.struct_time([2022, 1, 7, 9, 13, 14, 6, 0, 0]) + await knx.setup_integration( + { + CONF_KNX_EXPOSE: { + CONF_TYPE: "datetime", + KNX_ADDRESS: "1/1/8", + } + }, + ) + assert not hass.states.async_all() + + await knx.assert_write("1/1/8", (0x7A, 0x1, 0x7, 0xE9, 0xD, 0xE, 0x20, 0x80)) + + await knx.receive_read("1/1/8") + await knx.assert_response("1/1/8", (0x7A, 0x1, 0x7, 0xE9, 0xD, 0xE, 0x20, 0x80)) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert await hass.config_entries.async_unload(entries[0].entry_id) From 1a6964448ca953e4827f3fe8a784cc990c0a6b74 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 29 Jan 2022 14:32:12 +0100 Subject: [PATCH 058/298] Fix KNX Expose for strings longer than 14 bytes (#63026) * Fix KNX Expose for too long strings * Fix tests * Catch exception and avoid error during config entry setup for exposures * Properly catch exceptions in knx expose * Fix pylint * Fix CI * Add test for conversion error --- homeassistant/components/knx/expose.py | 23 +++++++-- tests/components/knx/test_expose.py | 69 +++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 34949b8c0b8..0963ec3be43 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -2,10 +2,12 @@ from __future__ import annotations from collections.abc import Callable +import logging from xknx import XKNX from xknx.devices import DateTime, ExposeSensor -from xknx.dpt import DPTNumeric +from xknx.dpt import DPTNumeric, DPTString +from xknx.exceptions import ConversionError from xknx.remote_value import RemoteValueSensor from homeassistant.const import ( @@ -22,6 +24,8 @@ from homeassistant.helpers.typing import ConfigType, StateType from .const import KNX_ADDRESS from .schema import ExposeSchema +_LOGGER = logging.getLogger(__name__) + @callback def create_knx_exposure( @@ -101,7 +105,10 @@ class KNXExposeSensor: """Initialize state of the exposure.""" init_state = self.hass.states.get(self.entity_id) state_value = self._get_expose_value(init_state) - self.device.sensor_value.value = state_value + try: + self.device.sensor_value.value = state_value + except ConversionError: + _LOGGER.exception("Error during sending of expose sensor value") @callback def shutdown(self) -> None: @@ -132,6 +139,13 @@ class KNXExposeSensor: and issubclass(self.device.sensor_value.dpt_class, DPTNumeric) ): return float(value) + if ( + value is not None + and isinstance(self.device.sensor_value, RemoteValueSensor) + and issubclass(self.device.sensor_value.dpt_class, DPTString) + ): + # DPT 16.000 only allows up to 14 Bytes + return str(value)[:14] return value async def _async_entity_changed(self, event: Event) -> None: @@ -148,7 +162,10 @@ class KNXExposeSensor: async def _async_set_knx_value(self, value: StateType) -> None: """Set new value on xknx ExposeSensor.""" - await self.device.set(value) + try: + await self.device.set(value) + except ConversionError: + _LOGGER.exception("Error during sending of expose sensor value") class KNXExposeTime: diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index cbe174127c4..e5030eef461 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -128,6 +128,73 @@ async def test_expose_attribute_with_default(hass: HomeAssistant, knx: KNXTestKi await knx.assert_write("1/1/8", (0,)) +async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit): + """Test an expose to send string values of up to 14 bytes only.""" + + entity_id = "fake.entity" + attribute = "fake_attribute" + await knx.setup_integration( + { + CONF_KNX_EXPOSE: { + CONF_TYPE: "string", + KNX_ADDRESS: "1/1/8", + CONF_ENTITY_ID: entity_id, + CONF_ATTRIBUTE: attribute, + ExposeSchema.CONF_KNX_EXPOSE_DEFAULT: "Test", + } + }, + ) + assert not hass.states.async_all() + + # Before init default value shall be sent as response + await knx.receive_read("1/1/8") + await knx.assert_response( + "1/1/8", (84, 101, 115, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + ) + + # Change attribute; keep state + hass.states.async_set( + entity_id, + "on", + {attribute: "This is a very long string that is larger than 14 bytes"}, + ) + await knx.assert_write( + "1/1/8", (84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 118, 101, 114, 121) + ) + + +async def test_expose_conversion_exception(hass: HomeAssistant, knx: KNXTestKit): + """Test expose throws exception.""" + + entity_id = "fake.entity" + attribute = "fake_attribute" + await knx.setup_integration( + { + CONF_KNX_EXPOSE: { + CONF_TYPE: "percent", + KNX_ADDRESS: "1/1/8", + CONF_ENTITY_ID: entity_id, + CONF_ATTRIBUTE: attribute, + ExposeSchema.CONF_KNX_EXPOSE_DEFAULT: 1, + } + }, + ) + assert not hass.states.async_all() + + # Before init default value shall be sent as response + await knx.receive_read("1/1/8") + await knx.assert_response("1/1/8", (3,)) + + # Change attribute: Expect no exception + hass.states.async_set( + entity_id, + "on", + {attribute: 101}, + ) + + await knx.assert_no_telegram() + + @patch("time.localtime") async def test_expose_with_date(localtime, hass: HomeAssistant, knx: KNXTestKit): """Test an expose with a date.""" @@ -138,7 +205,7 @@ async def test_expose_with_date(localtime, hass: HomeAssistant, knx: KNXTestKit) CONF_TYPE: "datetime", KNX_ADDRESS: "1/1/8", } - }, + } ) assert not hass.states.async_all() From 2ed20df906113f8faf65d0962ed4fc3ca2529fc7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 29 Jan 2022 16:13:59 +0100 Subject: [PATCH 059/298] Minor refactoring of cast media_player (#65125) --- homeassistant/components/cast/media_player.py | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e966e98c3f1..6cca4cfa20b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from contextlib import suppress from datetime import datetime, timedelta -import functools as ft import json import logging from urllib.parse import quote @@ -461,33 +460,10 @@ class CastDevice(MediaPlayerEntity): media_controller = self._media_controller() media_controller.seek(position) - async def async_browse_media(self, media_content_type=None, media_content_id=None): - """Implement the websocket media browsing helper.""" - kwargs = {} + async def _async_root_payload(self, content_filter): + """Generate root node.""" children = [] - - if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_AUDIO: - kwargs["content_filter"] = lambda item: item.media_content_type.startswith( - "audio/" - ) - - if media_content_id is not None: - if plex.is_plex_media_id(media_content_id): - return await plex.async_browse_media( - self.hass, - media_content_type, - media_content_id, - platform=CAST_DOMAIN, - ) - return await media_source.async_browse_media( - self.hass, media_content_id, **kwargs - ) - - if media_content_type == "plex": - return await plex.async_browse_media( - self.hass, None, None, platform=CAST_DOMAIN - ) - + # Add external sources if "plex" in self.hass.config.components: children.append( BrowseMedia( @@ -501,15 +477,17 @@ class CastDevice(MediaPlayerEntity): ) ) + # Add local media source try: result = await media_source.async_browse_media( - self.hass, media_content_id, **kwargs + self.hass, None, content_filter=content_filter ) children.append(result) except BrowseError: if not children: raise + # If there's only one media source, resolve it if len(children) == 1: return await self.async_browse_media( children[0].media_content_type, @@ -526,6 +504,34 @@ class CastDevice(MediaPlayerEntity): children=children, ) + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + content_filter = None + + if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_AUDIO: + + def audio_content_filter(item): + """Filter non audio content.""" + return item.media_content_type.startswith("audio/") + + content_filter = audio_content_filter + + if media_content_id is None: + return await self._async_root_payload(content_filter) + + if plex.is_plex_media_id(media_content_id): + return await plex.async_browse_media( + self.hass, media_content_type, media_content_id, platform=CAST_DOMAIN + ) + if media_content_type == "plex": + return await plex.async_browse_media( + self.hass, None, None, platform=CAST_DOMAIN + ) + + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=content_filter + ) + async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" # Handle media_source @@ -547,12 +553,6 @@ class CastDevice(MediaPlayerEntity): hass_url = get_url(self.hass, prefer_external=True) media_id = f"{hass_url}{media_id}" - await self.hass.async_add_executor_job( - ft.partial(self.play_media, media_type, media_id, **kwargs) - ) - - def play_media(self, media_type, media_id, **kwargs): - """Play media from a URL.""" extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) metadata = extra.get("metadata") @@ -571,7 +571,9 @@ class CastDevice(MediaPlayerEntity): if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) - self._chromecast.start_app(app_id) + await self.hass.async_add_executor_job( + self._chromecast.start_app, app_id + ) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", @@ -581,21 +583,28 @@ class CastDevice(MediaPlayerEntity): app_name = app_data.pop("app_name") try: - quick_play(self._chromecast, app_name, app_data) + await self.hass.async_add_executor_job( + quick_play, self._chromecast, app_name, app_data + ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) + # Handle plex elif media_id and media_id.startswith(PLEX_URI_SCHEME): media_id = media_id[len(PLEX_URI_SCHEME) :] - media = lookup_plex_media(self.hass, media_type, media_id) + media = await self.hass.async_add_executor_job( + lookup_plex_media, self.hass, media_type, media_id + ) if media is None: return controller = PlexController() self._chromecast.register_handler(controller) - controller.play_media(media) + await self.hass.async_add_executor_job(controller.play_media, media) else: app_data = {"media_id": media_id, "media_type": media_type, **extra} - quick_play(self._chromecast, "default_media_receiver", app_data) + await self.hass.async_add_executor_job( + quick_play, self._chromecast, "default_media_receiver", app_data + ) def _media_status(self): """ From b40bcecac078adf7d9f38539f57c57d576950a8f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 29 Jan 2022 14:01:46 +0100 Subject: [PATCH 060/298] Aqara restore door sensor state on start (#65128) * restore door sensor state on start * fix import * fix issues * also fix Natgas, WaterLeak and Smoke sensors * remove unnesesary async_schedule_update_ha_state --- .../components/xiaomi_aqara/binary_sensor.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 13d65cb21f4..ae4059728fe 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.restore_state import RestoreEntity from . import XiaomiDevice from .const import DOMAIN, GATEWAYS_KEY @@ -181,6 +182,11 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): attrs.update(super().extra_state_attributes) return attrs + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if DENSITY in data: @@ -232,6 +238,11 @@ class XiaomiMotionSensor(XiaomiBinarySensor): self._state = False self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway. @@ -293,7 +304,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor): return True -class XiaomiDoorSensor(XiaomiBinarySensor): +class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): """Representation of a XiaomiDoorSensor.""" def __init__(self, device, xiaomi_hub, config_entry): @@ -319,6 +330,15 @@ class XiaomiDoorSensor(XiaomiBinarySensor): attrs.update(super().extra_state_attributes) return attrs + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state is None: + return + + self._state = state.state == "on" + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" self._should_poll = False @@ -362,6 +382,11 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): config_entry, ) + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" self._should_poll = False @@ -400,6 +425,11 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): attrs.update(super().extra_state_attributes) return attrs + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._state = False + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if DENSITY in data: From 2041d4c11868dcf3e9d8891d587212db39634709 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 30 Jan 2022 14:12:01 -0700 Subject: [PATCH 061/298] Clean up SimpliSafe config flow tests (#65167) * Clean up SimpliSafe config flow tests * Cleanup --- tests/components/simplisafe/conftest.py | 81 +++++++ .../components/simplisafe/test_config_flow.py | 225 +++++------------- 2 files changed, 146 insertions(+), 160 deletions(-) create mode 100644 tests/components/simplisafe/conftest.py diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py new file mode 100644 index 00000000000..b793ee8656e --- /dev/null +++ b/tests/components/simplisafe/conftest.py @@ -0,0 +1,81 @@ +"""Define test fixtures for SimpliSafe.""" +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE +from homeassistant.components.simplisafe.const import CONF_USER_ID, DOMAIN +from homeassistant.const import CONF_TOKEN +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +REFRESH_TOKEN = "token123" +USER_ID = "12345" + + +@pytest.fixture(name="api") +def api_fixture(websocket): + """Define a fixture for a simplisafe-python API object.""" + return Mock( + async_get_systems=AsyncMock(), + refresh_token=REFRESH_TOKEN, + user_id=USER_ID, + websocket=websocket, + ) + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass, config): + """Define a config entry fixture.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=config) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture(name="config") +def config_fixture(hass): + """Define a config entry data fixture.""" + return { + CONF_USER_ID: USER_ID, + CONF_TOKEN: REFRESH_TOKEN, + } + + +@pytest.fixture(name="config_code") +def config_code_fixture(hass): + """Define a authorization code.""" + return { + CONF_AUTH_CODE: "code123", + } + + +@pytest.fixture(name="setup_simplisafe") +async def setup_simplisafe_fixture(hass, api, config): + """Define a fixture to set up SimpliSafe.""" + with patch( + "homeassistant.components.simplisafe.API.async_from_auth", return_value=api + ), patch( + "homeassistant.components.simplisafe.API.async_from_refresh_token", + return_value=api, + ), patch( + "homeassistant.components.simplisafe.SimpliSafe.async_init" + ), patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + return_value=api, + ), patch( + "homeassistant.components.simplisafe.PLATFORMS", [] + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield + + +@pytest.fixture(name="websocket") +def websocket_fixture(): + """Define a fixture for a simplisafe-python websocket object.""" + return Mock( + async_connect=AsyncMock(), + async_disconnect=AsyncMock(), + async_listen=AsyncMock(), + ) diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 0597ad377cf..2e8fe309ff2 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,51 +1,39 @@ """Define tests for the SimpliSafe config flow.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import patch import pytest from simplipy.errors import InvalidCredentialsError, SimplipyError from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN -from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE -from homeassistant.components.simplisafe.const import CONF_USER_ID from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME - -from tests.common import MockConfigEntry +from homeassistant.const import CONF_CODE -@pytest.fixture(name="api") -def api_fixture(): - """Define a fixture for simplisafe-python API object.""" - api = Mock() - api.refresh_token = "token123" - api.user_id = "12345" - return api - - -@pytest.fixture(name="mock_async_from_auth") -def mock_async_from_auth_fixture(api): - """Define a fixture for simplipy.API.async_from_auth.""" - with patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_auth", - ) as mock_async_from_auth: - mock_async_from_auth.side_effect = AsyncMock(return_value=api) - yield mock_async_from_auth - - -async def test_duplicate_error(hass, mock_async_from_auth): +async def test_duplicate_error(hass, config_entry, config_code, setup_simplisafe): """Test that errors are shown when duplicates are added.""" - MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_USER_ID: "12345", - CONF_TOKEN: "token123", - }, - ).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config_code + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "exc,error_string", + [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], +) +async def test_errors(hass, config_code, exc, error_string): + """Test that exceptions show the appropriate error.""" with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True + "homeassistant.components.simplisafe.API.async_from_auth", + side_effect=exc, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -54,135 +42,75 @@ async def test_duplicate_error(hass, mock_async_from_auth): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input=config_code ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": error_string} -async def test_invalid_credentials(hass, mock_async_from_auth): - """Test that invalid credentials show the correct error.""" - mock_async_from_auth.side_effect = AsyncMock(side_effect=InvalidCredentialsError) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} - - -async def test_options_flow(hass): +async def test_options_flow(hass, config_entry): """Test config flow options.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="abcde12345", - data={CONF_USER_ID: "12345", CONF_TOKEN: "token456"}, - options={CONF_CODE: "1234"}, - ) - entry.add_to_hass(hass) - with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): - await hass.config_entries.async_setup(entry.entry_id) - result = await hass.config_entries.options.async_init(entry.entry_id) - + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CODE: "4321"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert entry.options == {CONF_CODE: "4321"} + assert config_entry.options == {CONF_CODE: "4321"} -async def test_step_reauth_old_format(hass, mock_async_from_auth): +async def test_step_reauth_old_format( + hass, config, config_code, config_entry, setup_simplisafe +): """Test the re-auth step with "old" config entries (those with user IDs).""" - MockConfigEntry( - domain=DOMAIN, - unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - }, - ).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "user" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config_code + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} + assert config_entry.data == config -async def test_step_reauth_new_format(hass, mock_async_from_auth): +async def test_step_reauth_new_format( + hass, config, config_code, config_entry, setup_simplisafe +): """Test the re-auth step with "new" config entries (those with user IDs).""" - MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_USER_ID: "12345", - CONF_TOKEN: "token123", - }, - ).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={CONF_USER_ID: "12345", CONF_TOKEN: "token123"}, + DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "user" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config_code + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} + assert config_entry.data == config -async def test_step_reauth_wrong_account(hass, api, mock_async_from_auth): +async def test_step_reauth_wrong_account( + hass, api, config, config_code, config_entry, setup_simplisafe +): """Test the re-auth step returning a different account from this one.""" - MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_USER_ID: "12345", - CONF_TOKEN: "token123", - }, - ).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={CONF_USER_ID: "12345", CONF_TOKEN: "token123"}, + DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "user" @@ -190,52 +118,29 @@ async def test_step_reauth_wrong_account(hass, api, mock_async_from_auth): # identified as this entry's unique ID: api.user_id = "67890" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "wrong_account" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config_code + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "wrong_account" assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) assert config_entry.unique_id == "12345" -async def test_step_user(hass, mock_async_from_auth): +async def test_step_user(hass, config, config_code, setup_simplisafe): """Test the user step.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config_code + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} - - -async def test_unknown_error(hass, mock_async_from_auth): - """Test that an unknown error shows ohe correct error.""" - mock_async_from_auth.side_effect = AsyncMock(side_effect=SimplipyError) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} + assert config_entry.data == config From dcf6e61d4ffbed8f5fe2c8024234c5f543ae4fc4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 29 Jan 2022 13:30:15 -0700 Subject: [PATCH 062/298] Ensure diagnostics redaction can handle lists of lists (#65170) * Ensure diagnostics redaction can handle lists of lists * Code review * Update homeassistant/components/diagnostics/util.py Co-authored-by: Paulus Schoutsen * Code review * Typing * Revert "Typing" This reverts commit 8a57f772caa5180b609175591d81dfc473769f70. * New typing attempt * Revert "New typing attempt" This reverts commit e26e4aae69f62325fdd6af4d80c8fd1f74846e54. * Fix typing * Fix typing again * Add tests Co-authored-by: Paulus Schoutsen --- homeassistant/components/diagnostics/util.py | 11 +++++-- tests/components/diagnostics/test_util.py | 33 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 tests/components/diagnostics/test_util.py diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index d849dc052e4..6154dd14bd2 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -2,19 +2,24 @@ from __future__ import annotations from collections.abc import Iterable, Mapping -from typing import Any +from typing import Any, TypeVar, cast from homeassistant.core import callback from .const import REDACTED +T = TypeVar("T") + @callback -def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict[str, Any]: +def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: """Redact sensitive data in a dict.""" if not isinstance(data, (Mapping, list)): return data + if isinstance(data, list): + return cast(T, [async_redact_data(val, to_redact) for val in data]) + redacted = {**data} for key, value in redacted.items(): @@ -25,4 +30,4 @@ def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict[str, Any] elif isinstance(value, list): redacted[key] = [async_redact_data(item, to_redact) for item in value] - return redacted + return cast(T, redacted) diff --git a/tests/components/diagnostics/test_util.py b/tests/components/diagnostics/test_util.py new file mode 100644 index 00000000000..702b838334f --- /dev/null +++ b/tests/components/diagnostics/test_util.py @@ -0,0 +1,33 @@ +"""Test Diagnostics utils.""" +from homeassistant.components.diagnostics import REDACTED, async_redact_data + + +def test_redact(): + """Test the async_redact_data helper.""" + data = { + "key1": "value1", + "key2": ["value2_a", "value2_b"], + "key3": [["value_3a", "value_3b"], ["value_3c", "value_3d"]], + "key4": { + "key4_1": "value4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + } + + to_redact = { + "key1", + "key3", + "key4_1", + } + + assert async_redact_data(data, to_redact) == { + "key1": REDACTED, + "key2": ["value2_a", "value2_b"], + "key3": REDACTED, + "key4": { + "key4_1": REDACTED, + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + } From f6f25fa4ffef391eca2aedc763cd43336b310fb9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 30 Jan 2022 15:37:56 -0700 Subject: [PATCH 063/298] Add diagnostics to SimpliSafe (#65171) * Add diagnostics to SimpliSafe * Bump * Cleanup --- .../components/simplisafe/diagnostics.py | 40 ++ .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/simplisafe/conftest.py | 52 ++- .../fixtures/latest_event_data.json | 21 + .../simplisafe/fixtures/sensor_data.json | 75 ++++ .../simplisafe/fixtures/settings_data.json | 69 ++++ .../fixtures/subscription_data.json | 374 ++++++++++++++++++ .../components/simplisafe/test_diagnostics.py | 226 +++++++++++ 10 files changed, 853 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/simplisafe/diagnostics.py create mode 100644 tests/components/simplisafe/fixtures/latest_event_data.json create mode 100644 tests/components/simplisafe/fixtures/sensor_data.json create mode 100644 tests/components/simplisafe/fixtures/settings_data.json create mode 100644 tests/components/simplisafe/fixtures/subscription_data.json create mode 100644 tests/components/simplisafe/test_diagnostics.py diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py new file mode 100644 index 00000000000..bc0dddef47c --- /dev/null +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -0,0 +1,40 @@ +"""Diagnostics support for SimpliSafe.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant + +from . import SimpliSafe +from .const import DOMAIN + +CONF_SERIAL = "serial" +CONF_SYSTEM_ID = "system_id" +CONF_WIFI_SSID = "wifi_ssid" + +TO_REDACT = { + CONF_ADDRESS, + CONF_SERIAL, + CONF_SYSTEM_ID, + CONF_WIFI_SSID, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + simplisafe: SimpliSafe = hass.data[DOMAIN][entry.entry_id] + + return async_redact_data( + { + "entry": { + "options": dict(entry.options), + }, + "systems": [system.as_dict() for system in simplisafe.systems.values()], + }, + TO_REDACT, + ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8e494af013a..fa343e7466a 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.12.2"], + "requirements": ["simplisafe-python==2022.01.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3a3ae91fe2b..31e5cc0e5cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.2 +simplisafe-python==2022.01.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83086ebb786..929baafec7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.2 +simplisafe-python==2022.01.0 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index b793ee8656e..d9e6d46c2eb 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -1,24 +1,27 @@ """Define test fixtures for SimpliSafe.""" +import json from unittest.mock import AsyncMock, Mock, patch import pytest +from simplipy.system.v3 import SystemV3 from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.components.simplisafe.const import CONF_USER_ID, DOMAIN from homeassistant.const import CONF_TOKEN from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture REFRESH_TOKEN = "token123" +SYSTEM_ID = "system_123" USER_ID = "12345" @pytest.fixture(name="api") -def api_fixture(websocket): +def api_fixture(system_v3, websocket): """Define a fixture for a simplisafe-python API object.""" return Mock( - async_get_systems=AsyncMock(), + async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), refresh_token=REFRESH_TOKEN, user_id=USER_ID, websocket=websocket, @@ -50,19 +53,43 @@ def config_code_fixture(hass): } +@pytest.fixture(name="data_latest_event", scope="session") +def data_latest_event_fixture(): + """Define latest event data.""" + return json.loads(load_fixture("latest_event_data.json", "simplisafe")) + + +@pytest.fixture(name="data_sensor", scope="session") +def data_sensor_fixture(): + """Define sensor data.""" + return json.loads(load_fixture("sensor_data.json", "simplisafe")) + + +@pytest.fixture(name="data_settings", scope="session") +def data_settings_fixture(): + """Define settings data.""" + return json.loads(load_fixture("settings_data.json", "simplisafe")) + + +@pytest.fixture(name="data_subscription", scope="session") +def data_subscription_fixture(): + """Define subscription data.""" + return json.loads(load_fixture("subscription_data.json", "simplisafe")) + + @pytest.fixture(name="setup_simplisafe") async def setup_simplisafe_fixture(hass, api, config): """Define a fixture to set up SimpliSafe.""" with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + return_value=api, + ), patch( "homeassistant.components.simplisafe.API.async_from_auth", return_value=api ), patch( "homeassistant.components.simplisafe.API.async_from_refresh_token", return_value=api, ), patch( - "homeassistant.components.simplisafe.SimpliSafe.async_init" - ), patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_auth", - return_value=api, + "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" ), patch( "homeassistant.components.simplisafe.PLATFORMS", [] ): @@ -71,6 +98,17 @@ async def setup_simplisafe_fixture(hass, api, config): yield +@pytest.fixture(name="system_v3") +def system_v3_fixture(data_latest_event, data_sensor, data_settings, data_subscription): + """Define a fixture for a simplisafe-python V3 System object.""" + system = SystemV3(Mock(subscription_data=data_subscription), SYSTEM_ID) + system.async_get_latest_event = AsyncMock(return_value=data_latest_event) + system.sensor_data = data_sensor + system.settings_data = data_settings + system.generate_device_objects() + return system + + @pytest.fixture(name="websocket") def websocket_fixture(): """Define a fixture for a simplisafe-python websocket object.""" diff --git a/tests/components/simplisafe/fixtures/latest_event_data.json b/tests/components/simplisafe/fixtures/latest_event_data.json new file mode 100644 index 00000000000..ca44c0674f1 --- /dev/null +++ b/tests/components/simplisafe/fixtures/latest_event_data.json @@ -0,0 +1,21 @@ +{ + "eventId": 1234567890, + "eventTimestamp": 1564018073, + "eventCid": 1400, + "zoneCid": "2", + "sensorType": 1, + "sensorSerial": "01010101", + "account": "00011122", + "userId": 12345, + "sid": "system_123", + "info": "System Disarmed by PIN 2", + "pinName": "", + "sensorName": "Kitchen", + "messageSubject": "SimpliSafe System Disarmed", + "messageBody": "System Disarmed: Your SimpliSafe security system was ...", + "eventType": "activity", + "timezone": 2, + "locationOffset": -360, + "videoStartedBy": "", + "video": {} +} diff --git a/tests/components/simplisafe/fixtures/sensor_data.json b/tests/components/simplisafe/fixtures/sensor_data.json new file mode 100644 index 00000000000..073d51b0538 --- /dev/null +++ b/tests/components/simplisafe/fixtures/sensor_data.json @@ -0,0 +1,75 @@ +{ + "825": { + "type": 5, + "serial": "825", + "name": "Fire Door", + "setting": { + "instantTrigger": false, + "away2": 1, + "away": 1, + "home2": 1, + "home": 1, + "off": 0 + }, + "status": { + "triggered": false + }, + "flags": { + "swingerShutdown": false, + "lowBattery": false, + "offline": false + } + }, + "14": { + "type": 12, + "serial": "14", + "name": "Front Door", + "setting": { + "instantTrigger": false, + "away2": 1, + "away": 1, + "home2": 1, + "home": 1, + "off": 0 + }, + "status": { + "triggered": false + }, + "flags": { + "swingerShutdown": false, + "lowBattery": false, + "offline": false + } + }, + "987": { + "serial": "987", + "type": 16, + "status": { + "pinPadState": 0, + "lockState": 1, + "pinPadOffline": false, + "pinPadLowBattery": false, + "lockDisabled": false, + "lockLowBattery": false, + "calibrationErrDelta": 0, + "calibrationErrZero": 0, + "lockJamState": 0 + }, + "name": "Front Door", + "deviceGroupID": 1, + "firmwareVersion": "1.0.0", + "bootVersion": "1.0.0", + "setting": { + "autoLock": 3, + "away": 1, + "home": 1, + "awayToOff": 0, + "homeToOff": 1 + }, + "flags": { + "swingerShutdown": false, + "lowBattery": false, + "offline": false + } + } +} diff --git a/tests/components/simplisafe/fixtures/settings_data.json b/tests/components/simplisafe/fixtures/settings_data.json new file mode 100644 index 00000000000..2b617bb8663 --- /dev/null +++ b/tests/components/simplisafe/fixtures/settings_data.json @@ -0,0 +1,69 @@ +{ + "account": "12345012", + "settings": { + "normal": { + "wifiSSID": "MY_WIFI", + "alarmDuration": 240, + "alarmVolume": 3, + "doorChime": 2, + "entryDelayAway": 30, + "entryDelayAway2": 30, + "entryDelayHome": 30, + "entryDelayHome2": 30, + "exitDelayAway": 60, + "exitDelayAway2": 60, + "exitDelayHome": 0, + "exitDelayHome2": 0, + "lastUpdated": "2019-07-03T03:24:20.999Z", + "light": true, + "voicePrompts": 2, + "_id": "1197192618725121765212" + }, + "pins": { + "lastUpdated": "2019-07-04T20:47:44.016Z", + "_id": "asd6281526381253123", + "users": [ + { + "_id": "1271279d966212121124c7", + "pin": "3456", + "name": "Test 1" + }, + { + "_id": "1271279d966212121124c6", + "pin": "5423", + "name": "Test 2" + }, + { + "_id": "1271279d966212121124c5", + "pin": "", + "name": "" + }, + { + "_id": "1271279d966212121124c4", + "pin": "", + "name": "" + } + ], + "duress": { + "pin": "9876" + }, + "master": { + "pin": "1234" + } + } + }, + "basestationStatus": { + "lastUpdated": "2019-07-15T15:28:22.961Z", + "rfJamming": false, + "ethernetStatus": 4, + "gsmRssi": -73, + "gsmStatus": 3, + "backupBattery": 5293, + "wallPower": 5933, + "wifiRssi": -49, + "wifiStatus": 1, + "_id": "6128153715231t237123", + "encryptionErrors": [] + }, + "lastUpdated": 1562273264 +} diff --git a/tests/components/simplisafe/fixtures/subscription_data.json b/tests/components/simplisafe/fixtures/subscription_data.json new file mode 100644 index 00000000000..56731307e42 --- /dev/null +++ b/tests/components/simplisafe/fixtures/subscription_data.json @@ -0,0 +1,374 @@ +{ + "system_123": { + "uid": 12345, + "sid": "system_123", + "sStatus": 20, + "activated": 1445034752, + "planSku": "SSEDSM2", + "planName": "Interactive Monitoring", + "price": 24.99, + "currency": "USD", + "country": "US", + "expires": 1602887552, + "canceled": 0, + "extraTime": 0, + "creditCard": { + "lastFour": "", + "type": "", + "ppid": "ABCDE12345", + "uid": 12345 + }, + "time": 2628000, + "paymentProfileId": "ABCDE12345", + "features": { + "monitoring": true, + "alerts": true, + "online": true, + "hazard": true, + "video": true, + "cameras": 10, + "dispatch": true, + "proInstall": false, + "discount": 0, + "vipCS": false, + "medical": true, + "careVisit": false, + "storageDays": 30 + }, + "status": { + "hasBaseStation": true, + "isActive": true, + "monitoring": "Active" + }, + "subscriptionFeatures": { + "monitoredSensorsTypes": [ + "Entry", + "Motion", + "GlassBreak", + "Smoke", + "CO", + "Freeze", + "Water" + ], + "monitoredPanicConditions": [ + "Fire", + "Medical", + "Duress" + ], + "dispatchTypes": [ + "Police", + "Fire", + "Medical", + "Guard" + ], + "remoteControl": [ + "ArmDisarm", + "LockUnlock", + "ViewSettings", + "ConfigureSettings" + ], + "cameraFeatures": { + "liveView": true, + "maxRecordingCameras": 10, + "recordingStorageDays": 30, + "videoVerification": true + }, + "support": { + "level": "Basic", + "annualVisit": false, + "professionalInstall": false + }, + "cellCommunicationBackup": true, + "alertChannels": [ + "Push", + "SMS", + "Email" + ], + "alertTypes": [ + "Alarm", + "Error", + "Activity", + "Camera" + ], + "alarmModes": [ + "Alarm", + "SecretAlert", + "Disabled" + ], + "supportedIntegrations": [ + "GoogleAssistant", + "AmazonAlexa", + "AugustLock" + ], + "timeline": {} + }, + "dispatcher": "cops", + "dcid": 0, + "location": { + "sid": 12345, + "uid": 12345, + "lStatus": 10, + "account": "1234ABCD", + "street1": "1234 Main Street", + "street2": "", + "locationName": "", + "city": "Atlantis", + "county": "SEA", + "state": "UW", + "zip": "12345", + "country": "US", + "crossStreet": "River 1 and River 2", + "notes": "", + "residenceType": 2, + "numAdults": 2, + "numChildren": 0, + "locationOffset": -360, + "safeWord": "TRITON", + "signature": "Atlantis Citizen 1", + "timeZone": 2, + "primaryContacts": [ + { + "name": "John Doe", + "phone": "1234567890" + } + ], + "secondaryContacts": [ + { + "name": "Jane Doe", + "phone": "9876543210" + } + ], + "copsOptIn": false, + "certificateUri": "https://simplisafe.com/account2/12345/alarm-certificate/12345", + "nestStructureId": "", + "system": { + "serial": "1234ABCD", + "alarmState": "OFF", + "alarmStateTimestamp": 0, + "isAlarming": false, + "version": 3, + "capabilities": { + "setWifiOverCell": true, + "setDoorbellChimeVolume": true, + "outdoorBattCamera": true + }, + "temperature": 67, + "exitDelayRemaining": 60, + "cameras": [ + { + "staleSettingsTypes": [], + "upgradeWhitelisted": false, + "model": "SS001", + "uuid": "1234567890", + "uid": 12345, + "sid": 12345, + "cameraSettings": { + "cameraName": "Camera", + "pictureQuality": "720p", + "nightVision": "auto", + "statusLight": "off", + "micSensitivity": 100, + "micEnable": true, + "speakerVolume": 75, + "motionSensitivity": 0, + "shutterHome": "closedAlarmOnly", + "shutterAway": "open", + "shutterOff": "closedAlarmOnly", + "wifiSsid": "", + "canStream": false, + "canRecord": false, + "pirEnable": true, + "vaEnable": true, + "notificationsEnable": false, + "enableDoorbellNotification": true, + "doorbellChimeVolume": "off", + "privacyEnable": false, + "hdr": false, + "vaZoningEnable": false, + "vaZoningRows": 0, + "vaZoningCols": 0, + "vaZoningMask": [], + "maxDigitalZoom": 10, + "supportedResolutions": [ + "480p", + "720p" + ], + "admin": { + "IRLED": 0, + "pirSens": 0, + "statusLEDState": 1, + "lux": "lowLux", + "motionDetectionEnabled": false, + "motionThresholdZero": 0, + "motionThresholdOne": 10000, + "levelChangeDelayZero": 30, + "levelChangeDelayOne": 10, + "audioDetectionEnabled": false, + "audioChannelNum": 2, + "audioSampleRate": 16000, + "audioChunkBytes": 2048, + "audioSampleFormat": 3, + "audioSensitivity": 50, + "audioThreshold": 50, + "audioDirection": 0, + "bitRate": 284, + "longPress": 2000, + "kframe": 1, + "gopLength": 40, + "idr": 1, + "fps": 20, + "firmwareVersion": "2.6.1.107", + "netConfigVersion": "", + "camAgentVersion": "", + "lastLogin": 1600639997, + "lastLogout": 1600639944, + "pirSampleRateMs": 800, + "pirHysteresisHigh": 2, + "pirHysteresisLow": 10, + "pirFilterCoefficient": 1, + "logEnabled": true, + "logLevel": 3, + "logQDepth": 20, + "firmwareGroup": "public", + "irOpenThreshold": 445, + "irCloseThreshold": 840, + "irOpenDelay": 3, + "irCloseDelay": 3, + "irThreshold1x": 388, + "irThreshold2x": 335, + "irThreshold3x": 260, + "rssi": [ + [ + 1600935204, + -43 + ] + ], + "battery": [], + "dbm": 0, + "vmUse": 161592, + "resSet": 10540, + "uptime": 810043.74, + "wifiDisconnects": 1, + "wifiDriverReloads": 1, + "statsPeriod": 3600000, + "sarlaccDebugLogTypes": 0, + "odProcessingFps": 8, + "odObjectMinWidthPercent": 6, + "odObjectMinHeightPercent": 24, + "odEnableObjectDetection": true, + "odClassificationMask": 2, + "odClassificationConfidenceThreshold": 0.95, + "odEnableOverlay": false, + "odAnalyticsLib": 2, + "odSensitivity": 85, + "odEventObjectMask": 2, + "odLuxThreshold": 445, + "odLuxHysteresisHigh": 4, + "odLuxHysteresisLow": 4, + "odLuxSamplingFrequency": 30, + "odFGExtractorMode": 2, + "odVideoScaleFactor": 1, + "odSceneType": 1, + "odCameraView": 3, + "odCameraFOV": 2, + "odBackgroundLearnStationary": true, + "odBackgroundLearnStationarySpeed": 15, + "odClassifierQualityProfile": 1, + "odEnableVideoAnalyticsWhileStreaming": false, + "wlanMac": "XX:XX:XX:XX:XX:XX", + "region": "us-east-1", + "enableWifiAnalyticsLib": false, + "ivLicense": "" + }, + "pirLevel": "medium", + "odLevel": "medium" + }, + "__v": 0, + "cameraStatus": { + "firmwareVersion": "2.6.1.107", + "netConfigVersion": "", + "camAgentVersion": "", + "lastLogin": 1600639997, + "lastLogout": 1600639944, + "wlanMac": "XX:XX:XX:XX:XX:XX", + "fwDownloadVersion": "", + "fwDownloadPercentage": 0, + "recovered": false, + "recoveredFromVersion": "", + "_id": "1234567890", + "initErrors": [], + "speedTestTokenCreated": 1600235629 + }, + "supportedFeatures": { + "providers": { + "webrtc": "none", + "recording": "simplisafe", + "live": "simplisafe" + }, + "audioEncodings": [ + "speex" + ], + "resolutions": [ + "480p", + "720p" + ], + "_id": "1234567890", + "pir": true, + "videoAnalytics": false, + "privacyShutter": true, + "microphone": true, + "fullDuplexAudio": false, + "wired": true, + "networkSpeedTest": false, + "videoEncoding": "h264" + }, + "subscription": { + "enabled": true, + "freeTrialActive": false, + "freeTrialUsed": true, + "freeTrialEnds": 0, + "freeTrialExpires": 0, + "planSku": "SSVM1", + "price": 0, + "expires": 0, + "storageDays": 30, + "trialUsed": true, + "trialActive": false, + "trialExpires": 0 + }, + "status": "online" + } + ], + "connType": "wifi", + "stateUpdated": 1601502948, + "messages": [ + { + "_id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "textTemplate": "Power Outage - Backup battery in use.", + "data": { + "time": "2020-02-16T03:20:28+00:00" + }, + "text": "Power Outage - Backup battery in use.", + "code": "2000", + "filters": [], + "link": "http://link.to.info", + "linkLabel": "More Info", + "expiration": 0, + "category": "error", + "timestamp": 1581823228 + } + ], + "powerOutage": false, + "lastPowerOutage": 1581991064, + "lastSuccessfulWifiTS": 1601424776, + "isOffline": false + } + }, + "pinUnlocked": true, + "billDate": 1602887552, + "billInterval": 2628000, + "pinUnlockedBy": "pin", + "autoActivation": null + } +} diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py new file mode 100644 index 00000000000..d2c2866bf5b --- /dev/null +++ b/tests/components/simplisafe/test_diagnostics.py @@ -0,0 +1,226 @@ +"""Test SimpliSafe diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisafe): + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": {"options": {}}, + "systems": [ + { + "address": REDACTED, + "alarm_going_off": False, + "connection_type": "wifi", + "notifications": [], + "serial": REDACTED, + "state": 99, + "system_id": REDACTED, + "temperature": 67, + "version": 3, + "sensors": [ + { + "name": "Fire Door", + "serial": REDACTED, + "type": 5, + "error": False, + "low_battery": False, + "offline": False, + "settings": { + "instantTrigger": False, + "away2": 1, + "away": 1, + "home2": 1, + "home": 1, + "off": 0, + }, + "trigger_instantly": False, + "triggered": False, + }, + { + "name": "Front Door", + "serial": REDACTED, + "type": 12, + "error": False, + "low_battery": False, + "offline": False, + "settings": { + "instantTrigger": False, + "away2": 1, + "away": 1, + "home2": 1, + "home": 1, + "off": 0, + }, + "trigger_instantly": False, + "triggered": False, + }, + ], + "alarm_duration": 240, + "alarm_volume": 3, + "battery_backup_power_level": 5293, + "cameras": [ + { + "camera_settings": { + "cameraName": "Camera", + "pictureQuality": "720p", + "nightVision": "auto", + "statusLight": "off", + "micSensitivity": 100, + "micEnable": True, + "speakerVolume": 75, + "motionSensitivity": 0, + "shutterHome": "closedAlarmOnly", + "shutterAway": "open", + "shutterOff": "closedAlarmOnly", + "wifiSsid": "", + "canStream": False, + "canRecord": False, + "pirEnable": True, + "vaEnable": True, + "notificationsEnable": False, + "enableDoorbellNotification": True, + "doorbellChimeVolume": "off", + "privacyEnable": False, + "hdr": False, + "vaZoningEnable": False, + "vaZoningRows": 0, + "vaZoningCols": 0, + "vaZoningMask": [], + "maxDigitalZoom": 10, + "supportedResolutions": ["480p", "720p"], + "admin": { + "IRLED": 0, + "pirSens": 0, + "statusLEDState": 1, + "lux": "lowLux", + "motionDetectionEnabled": False, + "motionThresholdZero": 0, + "motionThresholdOne": 10000, + "levelChangeDelayZero": 30, + "levelChangeDelayOne": 10, + "audioDetectionEnabled": False, + "audioChannelNum": 2, + "audioSampleRate": 16000, + "audioChunkBytes": 2048, + "audioSampleFormat": 3, + "audioSensitivity": 50, + "audioThreshold": 50, + "audioDirection": 0, + "bitRate": 284, + "longPress": 2000, + "kframe": 1, + "gopLength": 40, + "idr": 1, + "fps": 20, + "firmwareVersion": "2.6.1.107", + "netConfigVersion": "", + "camAgentVersion": "", + "lastLogin": 1600639997, + "lastLogout": 1600639944, + "pirSampleRateMs": 800, + "pirHysteresisHigh": 2, + "pirHysteresisLow": 10, + "pirFilterCoefficient": 1, + "logEnabled": True, + "logLevel": 3, + "logQDepth": 20, + "firmwareGroup": "public", + "irOpenThreshold": 445, + "irCloseThreshold": 840, + "irOpenDelay": 3, + "irCloseDelay": 3, + "irThreshold1x": 388, + "irThreshold2x": 335, + "irThreshold3x": 260, + "rssi": [[1600935204, -43]], + "battery": [], + "dbm": 0, + "vmUse": 161592, + "resSet": 10540, + "uptime": 810043.74, + "wifiDisconnects": 1, + "wifiDriverReloads": 1, + "statsPeriod": 3600000, + "sarlaccDebugLogTypes": 0, + "odProcessingFps": 8, + "odObjectMinWidthPercent": 6, + "odObjectMinHeightPercent": 24, + "odEnableObjectDetection": True, + "odClassificationMask": 2, + "odClassificationConfidenceThreshold": 0.95, + "odEnableOverlay": False, + "odAnalyticsLib": 2, + "odSensitivity": 85, + "odEventObjectMask": 2, + "odLuxThreshold": 445, + "odLuxHysteresisHigh": 4, + "odLuxHysteresisLow": 4, + "odLuxSamplingFrequency": 30, + "odFGExtractorMode": 2, + "odVideoScaleFactor": 1, + "odSceneType": 1, + "odCameraView": 3, + "odCameraFOV": 2, + "odBackgroundLearnStationary": True, + "odBackgroundLearnStationarySpeed": 15, + "odClassifierQualityProfile": 1, + "odEnableVideoAnalyticsWhileStreaming": False, + "wlanMac": "XX:XX:XX:XX:XX:XX", + "region": "us-east-1", + "enableWifiAnalyticsLib": False, + "ivLicense": "", + }, + "pirLevel": "medium", + "odLevel": "medium", + }, + "camera_type": 0, + "name": "Camera", + "serial": REDACTED, + "shutter_open_when_away": True, + "shutter_open_when_home": False, + "shutter_open_when_off": False, + "status": "online", + "subscription_enabled": True, + }, + ], + "chime_volume": 2, + "entry_delay_away": 30, + "entry_delay_home": 30, + "exit_delay_away": 60, + "exit_delay_home": 0, + "gsm_strength": -73, + "light": True, + "locks": [ + { + "name": "Front Door", + "serial": REDACTED, + "type": 16, + "error": False, + "low_battery": False, + "offline": False, + "settings": { + "autoLock": 3, + "away": 1, + "home": 1, + "awayToOff": 0, + "homeToOff": 1, + }, + "disabled": False, + "lock_low_battery": False, + "pin_pad_low_battery": False, + "pin_pad_offline": False, + "state": 1, + } + ], + "offline": False, + "power_outage": False, + "rf_jamming": False, + "voice_prompt_volume": 2, + "wall_power_level": 5933, + "wifi_ssid": REDACTED, + "wifi_strength": -49, + } + ], + } From 14c969ef6d476ae0d6dd192ad86aa0573aca27e3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 30 Jan 2022 22:20:59 +0100 Subject: [PATCH 064/298] Better manage of nested lists (#65176) --- homeassistant/components/unifi/diagnostics.py | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 29845775409..4f27ff4ff4f 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -31,24 +31,42 @@ REDACT_WLANS = {"bc_filter_list", "x_passphrase"} @callback -def async_replace_data(data: Mapping, to_replace: dict[str, str]) -> dict[str, Any]: - """Replace sensitive data in a dict.""" - if not isinstance(data, (Mapping, list, set, tuple)): - return to_replace.get(data, data) - +def async_replace_dict_data( + data: Mapping, to_replace: dict[str, str] +) -> dict[str, Any]: + """Redact sensitive data in a dict.""" redacted = {**data} - - for key, value in redacted.items(): + for key, value in data.items(): if isinstance(value, dict): - redacted[key] = async_replace_data(value, to_replace) + redacted[key] = async_replace_dict_data(value, to_replace) elif isinstance(value, (list, set, tuple)): - redacted[key] = [async_replace_data(item, to_replace) for item in value] + redacted[key] = async_replace_list_data(value, to_replace) elif isinstance(value, str): if value in to_replace: redacted[key] = to_replace[value] elif value.count(":") == 5: redacted[key] = REDACTED + return redacted + +@callback +def async_replace_list_data( + data: list | set | tuple, to_replace: dict[str, str] +) -> list[Any]: + """Redact sensitive data in a list.""" + redacted = [] + for item in data: + new_value = None + if isinstance(item, (list, set, tuple)): + new_value = async_replace_list_data(item, to_replace) + elif isinstance(item, Mapping): + new_value = async_replace_dict_data(item, to_replace) + elif isinstance(item, str): + if item in to_replace: + new_value = to_replace[item] + elif item.count(":") == 5: + new_value = REDACTED + redacted.append(new_value or item) return redacted @@ -73,26 +91,28 @@ async def async_get_config_entry_diagnostics( counter += 1 diag["config"] = async_redact_data( - async_replace_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG + async_replace_dict_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG ) diag["site_role"] = controller.site_role - diag["entities"] = async_replace_data(controller.entities, macs_to_redact) + diag["entities"] = async_replace_dict_data(controller.entities, macs_to_redact) diag["clients"] = { macs_to_redact[k]: async_redact_data( - async_replace_data(v.raw, macs_to_redact), REDACT_CLIENTS + async_replace_dict_data(v.raw, macs_to_redact), REDACT_CLIENTS ) for k, v in controller.api.clients.items() } diag["devices"] = { macs_to_redact[k]: async_redact_data( - async_replace_data(v.raw, macs_to_redact), REDACT_DEVICES + async_replace_dict_data(v.raw, macs_to_redact), REDACT_DEVICES ) for k, v in controller.api.devices.items() } diag["dpi_apps"] = {k: v.raw for k, v in controller.api.dpi_apps.items()} diag["dpi_groups"] = {k: v.raw for k, v in controller.api.dpi_groups.items()} diag["wlans"] = { - k: async_redact_data(async_replace_data(v.raw, macs_to_redact), REDACT_WLANS) + k: async_redact_data( + async_replace_dict_data(v.raw, macs_to_redact), REDACT_WLANS + ) for k, v in controller.api.wlans.items() } From d6527953c39cfa278b55e859d59a617802696a23 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Jan 2022 22:22:32 +0100 Subject: [PATCH 065/298] Fix "internet access" switch for Fritz connected device without known IP address (#65190) * fix get wan access * small improvement - default wan_access to None - test if dev_info.ip_address is not empty --- homeassistant/components/fritz/common.py | 26 +++++++++++++++--------- homeassistant/components/fritz/switch.py | 9 +++++++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 9d1c4543857..70485ac0c5f 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -107,7 +107,7 @@ class Device: ip_address: str name: str ssid: str | None - wan_access: bool = True + wan_access: bool | None = None class Interface(TypedDict): @@ -277,6 +277,14 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): ) return bool(version), version + def _get_wan_access(self, ip_address: str) -> bool | None: + """Get WAN access rule for given IP address.""" + return not self.connection.call_action( + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=ip_address, + ).get("NewDisallow") + async def async_scan_devices(self, now: datetime | None = None) -> None: """Wrap up FritzboxTools class scan.""" await self.hass.async_add_executor_job(self.scan_devices, now) @@ -315,7 +323,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): connection_type="", ip_address=host["ip"], ssid=None, - wan_access=False, + wan_access=None, ) mesh_intf = {} @@ -352,12 +360,10 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) if intf is not None: - if intf["op_mode"] != "AP_GUEST": - dev_info.wan_access = not self.connection.call_action( - "X_AVM-DE_HostFilter:1", - "GetWANAccessByIP", - NewIPv4Address=dev_info.ip_address, - ).get("NewDisallow") + if intf["op_mode"] != "AP_GUEST" and dev_info.ip_address: + dev_info.wan_access = self._get_wan_access( + dev_info.ip_address + ) dev_info.connected_to = intf["device"] dev_info.connection_type = intf["type"] @@ -762,7 +768,7 @@ class FritzDevice: self._mac = mac self._name = name self._ssid: str | None = None - self._wan_access = False + self._wan_access: bool | None = False def update(self, dev_info: Device, consider_home: float) -> None: """Update device info.""" @@ -830,7 +836,7 @@ class FritzDevice: return self._ssid @property - def wan_access(self) -> bool: + def wan_access(self) -> bool | None: """Return device wan access.""" return self._wan_access diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 0726741845a..fec760fbe7a 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -477,10 +477,17 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): self._attr_entity_category = EntityCategory.CONFIG @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Switch status.""" return self._avm_wrapper.devices[self._mac].wan_access + @property + def available(self) -> bool: + """Return availability of the switch.""" + if self._avm_wrapper.devices[self._mac].wan_access is None: + return False + return super().available + @property def device_info(self) -> DeviceInfo: """Return the device information.""" From 5368fb6d542abf0e1c1fcfbb3cbddeadff8b0a64 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 30 Jan 2022 01:15:49 +0200 Subject: [PATCH 066/298] Fix webostv configure sources when selected source is missing (#65195) * Fix webostv configure sources when selected source is missing * Add comment for filtering duplicates --- homeassistant/components/webostv/config_flow.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 2120635be91..3bf4f7c6aeb 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -178,11 +178,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_input = {CONF_SOURCES: user_input[CONF_SOURCES]} return self.async_create_entry(title="", data=options_input) # Get sources - sources = self.options.get(CONF_SOURCES, "") sources_list = await async_get_sources(self.host, self.key) if not sources_list: errors["base"] = "cannot_retrieve" + sources = [s for s in self.options.get(CONF_SOURCES, []) if s in sources_list] + if not sources: + sources = sources_list + options_schema = vol.Schema( { vol.Optional( @@ -204,7 +207,11 @@ async def async_get_sources(host: str, key: str) -> list[str]: except WEBOSTV_EXCEPTIONS: return [] - return [ - *(app["title"] for app in client.apps.values()), - *(app["label"] for app in client.inputs.values()), - ] + return list( + dict.fromkeys( # Preserve order when filtering duplicates + [ + *(app["title"] for app in client.apps.values()), + *(app["label"] for app in client.inputs.values()), + ] + ) + ) From 508fd0cb2a4e9d162fcf3c599163a3435097ab2e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 30 Jan 2022 00:11:28 +0100 Subject: [PATCH 067/298] Add logic to avoid creating the same scene multiple times (#65207) --- homeassistant/components/deconz/scene.py | 11 ++++++-- tests/components/deconz/test_diagnostics.py | 1 + tests/components/deconz/test_scene.py | 29 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 3d8e1aa27ba..9fcccc52386 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -7,7 +7,7 @@ from typing import Any from pydeconz.group import DeconzScene as PydeconzScene -from homeassistant.components.scene import Scene +from homeassistant.components.scene import DOMAIN, Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -23,6 +23,7 @@ async def async_setup_entry( ) -> None: """Set up scenes for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() @callback def async_add_scene( @@ -30,7 +31,11 @@ async def async_setup_entry( | ValuesView[PydeconzScene] = gateway.api.scenes.values(), ) -> None: """Add scene from deCONZ.""" - entities = [DeconzScene(scene, gateway) for scene in scenes] + entities = [ + DeconzScene(scene, gateway) + for scene in scenes + if scene.deconz_id not in gateway.entities[DOMAIN] + ] if entities: async_add_entities(entities) @@ -59,10 +64,12 @@ class DeconzScene(Scene): async def async_added_to_hass(self) -> None: """Subscribe to sensors events.""" self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id + self.gateway.entities[DOMAIN].add(self._scene.deconz_id) async def async_will_remove_from_hass(self) -> None: """Disconnect scene object when removed.""" del self.gateway.deconz_ids[self.entity_id] + self.gateway.entities[DOMAIN].remove(self._scene.deconz_id) self._scene = None async def async_activate(self, **kwargs: Any) -> None: diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index 7d351a333cf..17da9f1141a 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -55,6 +55,7 @@ async def test_entry_diagnostics( str(Platform.LIGHT): [], str(Platform.LOCK): [], str(Platform.NUMBER): [], + str(Platform.SCENE): [], str(Platform.SENSOR): [], str(Platform.SIREN): [], str(Platform.SWITCH): [], diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 189eb1e6eb7..e6f74cd0529 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -2,8 +2,10 @@ from unittest.mock import patch +from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.dispatcher import async_dispatcher_send from .test_gateway import ( DECONZ_WEB_REQUEST, @@ -58,3 +60,30 @@ async def test_scenes(hass, aioclient_mock): await hass.config_entries.async_unload(config_entry.entry_id) assert len(hass.states.async_all()) == 0 + + +async def test_only_new_scenes_are_created(hass, aioclient_mock): + """Test that scenes works.""" + data = { + "groups": { + "1": { + "id": "Light group id", + "name": "Light group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene"}], + "lights": [], + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + + gateway = get_gateway_from_config_entry(hass, config_entry) + async_dispatcher_send(hass, gateway.signal_new_scene) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 From 305ffc4ab67752c71966e319626cf42a57495d55 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 30 Jan 2022 15:15:51 -0600 Subject: [PATCH 068/298] Add activity statistics to Sonos diagnostics (#65214) --- homeassistant/components/sonos/__init__.py | 1 + homeassistant/components/sonos/diagnostics.py | 1 + homeassistant/components/sonos/speaker.py | 4 +- homeassistant/components/sonos/statistics.py | 50 +++++++++++++++---- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 27b9d720b0f..673e0ac4dfe 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -193,6 +193,7 @@ class SonosDiscoveryManager: async def _async_stop_event_listener(self, event: Event | None = None) -> None: for speaker in self.data.discovered.values(): + speaker.activity_stats.log_report() speaker.event_stats.log_report() await asyncio.gather( *(speaker.async_offline() for speaker in self.data.discovered.values()) diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 007348a66bb..707449e4002 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -130,5 +130,6 @@ async def async_generate_speaker_info( if s is speaker } payload["media"] = await async_generate_media_info(hass, speaker) + payload["activity_stats"] = speaker.activity_stats.report() payload["event_stats"] = speaker.event_stats.report() return payload diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4de0638d10c..5f06fe976ac 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -62,7 +62,7 @@ from .const import ( ) from .favorites import SonosFavorites from .helpers import soco_error -from .statistics import EventStatistics +from .statistics import ActivityStatistics, EventStatistics NEVER_TIME = -1200.0 EVENT_CHARGING = { @@ -177,6 +177,7 @@ class SonosSpeaker: self._event_dispatchers: dict[str, Callable] = {} self._last_activity: float = NEVER_TIME self._last_event_cache: dict[str, Any] = {} + self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name) self.event_stats: EventStatistics = EventStatistics(self.zone_name) # Scheduled callback handles @@ -528,6 +529,7 @@ class SonosSpeaker: """Track the last activity on this speaker, set availability and resubscribe.""" _LOGGER.debug("Activity on %s from %s", self.zone_name, source) self._last_activity = time.monotonic() + self.activity_stats.activity(source, self._last_activity) was_available = self.available self.available = True if not was_available: diff --git a/homeassistant/components/sonos/statistics.py b/homeassistant/components/sonos/statistics.py index 8cade9b0aa5..a850e5a8caf 100644 --- a/homeassistant/components/sonos/statistics.py +++ b/homeassistant/components/sonos/statistics.py @@ -9,13 +9,49 @@ from soco.events_base import Event as SonosEvent, parse_event_xml _LOGGER = logging.getLogger(__name__) -class EventStatistics: +class SonosStatistics: + """Base class of Sonos statistics.""" + + def __init__(self, zone_name: str, kind: str) -> None: + """Initialize SonosStatistics.""" + self._stats = {} + self._stat_type = kind + self.zone_name = zone_name + + def report(self) -> dict: + """Generate a report for use in diagnostics.""" + return self._stats.copy() + + def log_report(self) -> None: + """Log statistics for this speaker.""" + _LOGGER.debug( + "%s statistics for %s: %s", + self._stat_type, + self.zone_name, + self.report(), + ) + + +class ActivityStatistics(SonosStatistics): + """Representation of Sonos activity statistics.""" + + def __init__(self, zone_name: str) -> None: + """Initialize ActivityStatistics.""" + super().__init__(zone_name, "Activity") + + def activity(self, source: str, timestamp: float) -> None: + """Track an activity occurrence.""" + activity_entry = self._stats.setdefault(source, {"count": 0}) + activity_entry["count"] += 1 + activity_entry["last_seen"] = timestamp + + +class EventStatistics(SonosStatistics): """Representation of Sonos event statistics.""" def __init__(self, zone_name: str) -> None: """Initialize EventStatistics.""" - self._stats = {} - self.zone_name = zone_name + super().__init__(zone_name, "Event") def receive(self, event: SonosEvent) -> None: """Mark a received event by subscription type.""" @@ -38,11 +74,3 @@ class EventStatistics: payload["soco:from_didl_string"] = from_didl_string.cache_info() payload["soco:parse_event_xml"] = parse_event_xml.cache_info() return payload - - def log_report(self) -> None: - """Log event statistics for this speaker.""" - _LOGGER.debug( - "Event statistics for %s: %s", - self.zone_name, - self.report(), - ) From eca3514f9e0ae352ad1137f1c3e654d95f5e2486 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:24:42 -0600 Subject: [PATCH 069/298] Fix senseme fan lights (#65217) --- homeassistant/components/senseme/light.py | 78 ++++++++++------ tests/components/senseme/__init__.py | 74 ++++++++++------ tests/components/senseme/test_light.py | 103 ++++++++++++++++++++++ 3 files changed, 200 insertions(+), 55 deletions(-) create mode 100644 tests/components/senseme/test_light.py diff --git a/homeassistant/components/senseme/light.py b/homeassistant/components/senseme/light.py index 2a4d82de6d0..75d853c4001 100644 --- a/homeassistant/components/senseme/light.py +++ b/homeassistant/components/senseme/light.py @@ -31,50 +31,30 @@ async def async_setup_entry( ) -> None: """Set up SenseME lights.""" device = hass.data[DOMAIN][entry.entry_id] - if device.has_light: - async_add_entities([HASensemeLight(device)]) + if not device.has_light: + return + if device.is_light: + async_add_entities([HASensemeStandaloneLight(device)]) + else: + async_add_entities([HASensemeFanLight(device)]) class HASensemeLight(SensemeEntity, LightEntity): """Representation of a Big Ass Fans SenseME light.""" - def __init__(self, device: SensemeDevice) -> None: + def __init__(self, device: SensemeDevice, name: str) -> None: """Initialize the entity.""" - self._device = device - if device.is_light: - name = device.name # The device itself is a light - else: - name = f"{device.name} Light" # A fan light super().__init__(device, name) - if device.is_light: - self._attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP} - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - else: - self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} - self._attr_color_mode = COLOR_MODE_BRIGHTNESS - self._attr_unique_id = f"{self._device.uuid}-LIGHT" # for legacy compat - self._attr_min_mireds = color_temperature_kelvin_to_mired( - self._device.light_color_temp_max - ) - self._attr_max_mireds = color_temperature_kelvin_to_mired( - self._device.light_color_temp_min - ) + self._attr_unique_id = f"{device.uuid}-LIGHT" # for legacy compat @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" self._attr_is_on = self._device.light_on self._attr_brightness = int(min(255, self._device.light_brightness * 16)) - self._attr_color_temp = color_temperature_kelvin_to_mired( - self._device.light_color_temp - ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: - self._device.light_color_temp = color_temperature_mired_to_kelvin( - color_temp - ) if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: # set the brightness, which will also turn on/off light if brightness == 255: @@ -86,3 +66,45 @@ class HASensemeLight(SensemeEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" self._device.light_on = False + + +class HASensemeFanLight(HASensemeLight): + """Representation of a Big Ass Fans SenseME light on a fan.""" + + def __init__(self, device: SensemeDevice) -> None: + """Init a fan light.""" + super().__init__(device, device.name) + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + + +class HASensemeStandaloneLight(HASensemeLight): + """Representation of a Big Ass Fans SenseME light.""" + + def __init__(self, device: SensemeDevice) -> None: + """Init a standalone light.""" + super().__init__(device, f"{device.name} Light") + self._attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP} + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + self._attr_min_mireds = color_temperature_kelvin_to_mired( + device.light_color_temp_max + ) + self._attr_max_mireds = color_temperature_kelvin_to_mired( + device.light_color_temp_min + ) + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + super()._async_update_attrs() + self._attr_color_temp = color_temperature_kelvin_to_mired( + self._device.light_color_temp + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: + self._device.light_color_temp = color_temperature_mired_to_kelvin( + color_temp + ) + await super().async_turn_on(**kwargs) diff --git a/tests/components/senseme/__init__.py b/tests/components/senseme/__init__.py index 5c586d88fd5..8c9a7669889 100644 --- a/tests/components/senseme/__init__.py +++ b/tests/components/senseme/__init__.py @@ -12,32 +12,38 @@ MOCK_UUID = "77a6b7b3-925d-4695-a415-76d76dca4444" MOCK_ADDRESS = "127.0.0.1" MOCK_MAC = "20:F8:5E:92:5A:75" -device = MagicMock(auto_spec=SensemeDevice) -device.async_update = AsyncMock() -device.model = "Haiku Fan" -device.fan_speed_max = 7 -device.mac = "aa:bb:cc:dd:ee:ff" -device.fan_dir = "REV" -device.room_name = "Main" -device.room_type = "Main" -device.fw_version = "1" -device.fan_autocomfort = "on" -device.fan_smartmode = "on" -device.fan_whoosh_mode = "on" -device.name = MOCK_NAME -device.uuid = MOCK_UUID -device.address = MOCK_ADDRESS -device.get_device_info = { - "name": MOCK_NAME, - "uuid": MOCK_UUID, - "mac": MOCK_ADDRESS, - "address": MOCK_ADDRESS, - "base_model": "FAN,HAIKU,HSERIES", - "has_light": False, - "has_sensor": True, - "is_fan": True, - "is_light": False, -} + +def _mock_device(): + device = MagicMock(auto_spec=SensemeDevice) + device.async_update = AsyncMock() + device.model = "Haiku Fan" + device.fan_speed_max = 7 + device.mac = "aa:bb:cc:dd:ee:ff" + device.fan_dir = "REV" + device.has_light = True + device.is_light = False + device.light_brightness = 50 + device.room_name = "Main" + device.room_type = "Main" + device.fw_version = "1" + device.fan_autocomfort = "COOLING" + device.fan_smartmode = "OFF" + device.fan_whoosh_mode = "on" + device.name = MOCK_NAME + device.uuid = MOCK_UUID + device.address = MOCK_ADDRESS + device.get_device_info = { + "name": MOCK_NAME, + "uuid": MOCK_UUID, + "mac": MOCK_ADDRESS, + "address": MOCK_ADDRESS, + "base_model": "FAN,HAIKU,HSERIES", + "has_light": False, + "has_sensor": True, + "is_fan": True, + "is_light": False, + } + return device device_alternate_ip = MagicMock(auto_spec=SensemeDevice) @@ -99,7 +105,7 @@ device_no_uuid = MagicMock(auto_spec=SensemeDevice) device_no_uuid.uuid = None -MOCK_DEVICE = device +MOCK_DEVICE = _mock_device() MOCK_DEVICE_ALTERNATE_IP = device_alternate_ip MOCK_DEVICE2 = device2 MOCK_DEVICE_NO_UUID = device_no_uuid @@ -121,3 +127,17 @@ def _patch_discovery(device=None, no_device=None): yield return _patcher() + + +def _patch_device(device=None, no_device=False): + async def _device_mocker(*args, **kwargs): + if no_device: + return False, None + if device: + return True, device + return True, _mock_device() + + return patch( + "homeassistant.components.senseme.async_get_device_by_device_info", + new=_device_mocker, + ) diff --git a/tests/components/senseme/test_light.py b/tests/components/senseme/test_light.py new file mode 100644 index 00000000000..21811452610 --- /dev/null +++ b/tests/components/senseme/test_light.py @@ -0,0 +1,103 @@ +"""Tests for senseme light platform.""" + + +from aiosenseme import SensemeDevice + +from homeassistant.components import senseme +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.components.senseme.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import _mock_device, _patch_device, _patch_discovery + +from tests.common import MockConfigEntry + + +async def _setup_mocked_entry(hass: HomeAssistant, device: SensemeDevice) -> None: + """Set up a mocked entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"info": device.get_device_info}, + unique_id=device.uuid, + ) + entry.add_to_hass(hass) + with _patch_discovery(), _patch_device(device=device): + await async_setup_component(hass, senseme.DOMAIN, {senseme.DOMAIN: {}}) + await hass.async_block_till_done() + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + device = _mock_device() + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == f"{device.uuid}-LIGHT" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_fan_light(hass: HomeAssistant) -> None: + """Test a fan light.""" + device = _mock_device() + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_BRIGHTNESS] + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is False + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is True + + +async def test_standalone_light(hass: HomeAssistant) -> None: + """Test a standalone light.""" + device = _mock_device() + device.is_light = True + device.light_color_temp_max = 6500 + device.light_color_temp_min = 2700 + device.light_color_temp = 4000 + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan_light" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_COLOR_TEMP] + assert attributes[ATTR_COLOR_TEMP] == 250 + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is False + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is True From 26905115c805fb6901500dd257894a54ab4e4a22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:21:54 -0600 Subject: [PATCH 070/298] Increase the timeout for flux_led directed discovery (#65222) --- homeassistant/components/flux_led/const.py | 1 + homeassistant/components/flux_led/discovery.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 95121b5685b..9113d52f5c8 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -51,6 +51,7 @@ FLUX_LED_EXCEPTIONS: Final = ( STARTUP_SCAN_TIMEOUT: Final = 5 DISCOVER_SCAN_TIMEOUT: Final = 10 +DIRECTED_DISCOVERY_TIMEOUT: Final = 15 CONF_MODEL: Final = "model" CONF_MODEL_NUM: Final = "model_num" diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 32b0d0ed9df..0f65c7c1797 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -38,7 +38,7 @@ from .const import ( CONF_REMOTE_ACCESS_ENABLED, CONF_REMOTE_ACCESS_HOST, CONF_REMOTE_ACCESS_PORT, - DISCOVER_SCAN_TIMEOUT, + DIRECTED_DISCOVERY_TIMEOUT, DOMAIN, FLUX_LED_DISCOVERY, ) @@ -194,7 +194,7 @@ async def async_discover_device( """Direct discovery at a single ip instead of broadcast.""" # If we are missing the unique_id we should be able to fetch it # from the device by doing a directed discovery at the host only - for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): + for device in await async_discover_devices(hass, DIRECTED_DISCOVERY_TIMEOUT, host): if device[ATTR_IPADDR] == host: return device return None From 8e71e2e8ee812646833e1c66521d2befd4b4d6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 30 Jan 2022 22:09:36 +0100 Subject: [PATCH 071/298] Use .json.txt for diagnostics download filetype (#65236) --- homeassistant/components/diagnostics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index a0f161b23ae..f8a38971a95 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -170,7 +170,7 @@ async def _async_get_json_file_response( return web.Response( body=json_data, content_type="application/json", - headers={"Content-Disposition": f'attachment; filename="{filename}.json"'}, + headers={"Content-Disposition": f'attachment; filename="{filename}.json.txt"'}, ) From 6e4c281e1549179e17f1d478d3314cb629e046ee Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 30 Jan 2022 23:05:30 +0200 Subject: [PATCH 072/298] Fix webostv live TV source missing when configuring sources (#65243) --- .../components/webostv/config_flow.py | 18 +------- homeassistant/components/webostv/helpers.py | 30 ++++++++++++- tests/components/webostv/__init__.py | 21 +-------- tests/components/webostv/conftest.py | 16 ++----- tests/components/webostv/const.py | 36 +++++++++++++++ tests/components/webostv/test_config_flow.py | 44 ++++++++++++++----- .../components/webostv/test_device_trigger.py | 3 +- tests/components/webostv/test_init.py | 2 +- tests/components/webostv/test_media_player.py | 3 +- tests/components/webostv/test_notify.py | 3 +- tests/components/webostv/test_trigger.py | 3 +- 11 files changed, 113 insertions(+), 66 deletions(-) create mode 100644 tests/components/webostv/const.py diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 3bf4f7c6aeb..4c8ff6e5fd3 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.helpers.typing import ConfigType from . import async_control_connect from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS +from .helpers import async_get_sources DATA_SCHEMA = vol.Schema( { @@ -198,20 +199,3 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form( step_id="init", data_schema=options_schema, errors=errors ) - - -async def async_get_sources(host: str, key: str) -> list[str]: - """Construct sources list.""" - try: - client = await async_control_connect(host, key) - except WEBOSTV_EXCEPTIONS: - return [] - - return list( - dict.fromkeys( # Preserve order when filtering duplicates - [ - *(app["title"] for app in client.apps.values()), - *(app["label"] for app in client.inputs.values()), - ] - ) - ) diff --git a/homeassistant/components/webostv/helpers.py b/homeassistant/components/webostv/helpers.py index d3f5fec6826..70a253d5ceb 100644 --- a/homeassistant/components/webostv/helpers.py +++ b/homeassistant/components/webostv/helpers.py @@ -6,8 +6,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from . import WebOsClientWrapper -from .const import DATA_CONFIG_ENTRY, DOMAIN +from . import WebOsClientWrapper, async_control_connect +from .const import DATA_CONFIG_ENTRY, DOMAIN, LIVE_TV_APP_ID, WEBOSTV_EXCEPTIONS @callback @@ -81,3 +81,29 @@ def async_get_client_wrapper_by_device_entry( ) return wrapper + + +async def async_get_sources(host: str, key: str) -> list[str]: + """Construct sources list.""" + try: + client = await async_control_connect(host, key) + except WEBOSTV_EXCEPTIONS: + return [] + + sources = [] + found_live_tv = False + for app in client.apps.values(): + sources.append(app["title"]) + if app["id"] == LIVE_TV_APP_ID: + found_live_tv = True + + for source in client.inputs.values(): + sources.append(source["label"]) + if source["appId"] == LIVE_TV_APP_ID: + found_live_tv = True + + if not found_live_tv: + sources.append("Live TV") + + # Preserve order when filtering duplicates + return list(dict.fromkeys(sources)) diff --git a/tests/components/webostv/__init__.py b/tests/components/webostv/__init__.py index d6e2505b96c..1cbc72b43fc 100644 --- a/tests/components/webostv/__init__.py +++ b/tests/components/webostv/__init__.py @@ -11,27 +11,10 @@ from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_CLIENT_KEYS, TV_NAME + from tests.common import MockConfigEntry -FAKE_UUID = "some-fake-uuid" -TV_NAME = "fake_webos" -ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}" -HOST = "1.2.3.4" -CLIENT_KEY = "some-secret" -MOCK_CLIENT_KEYS = {HOST: CLIENT_KEY} -MOCK_JSON = '{"1.2.3.4": "some-secret"}' - -CHANNEL_1 = { - "channelNumber": "1", - "channelName": "Channel 1", - "channelId": "ch1id", -} -CHANNEL_2 = { - "channelNumber": "20", - "channelName": "Channel Name 2", - "channelId": "ch2id", -} - async def setup_webostv(hass, unique_id=FAKE_UUID): """Initialize webostv and media_player for tests.""" diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index 23c8687018c..05f1be66d00 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.webostv.const import LIVE_TV_APP_ID from homeassistant.helpers import entity_registry -from . import CHANNEL_1, CHANNEL_2, CLIENT_KEY, FAKE_UUID +from .const import CHANNEL_1, CHANNEL_2, CLIENT_KEY, FAKE_UUID, MOCK_APPS, MOCK_INPUTS from tests.common import async_mock_service @@ -28,18 +28,8 @@ def client_fixture(): client.software_info = {"major_ver": "major", "minor_ver": "minor"} client.system_info = {"modelName": "TVFAKE"} client.client_key = CLIENT_KEY - client.apps = { - LIVE_TV_APP_ID: { - "title": "Live TV", - "id": LIVE_TV_APP_ID, - "largeIcon": "large-icon", - "icon": "icon", - }, - } - client.inputs = { - "in1": {"label": "Input01", "id": "in1", "appId": "app0"}, - "in2": {"label": "Input02", "id": "in2", "appId": "app1"}, - } + client.apps = MOCK_APPS + client.inputs = MOCK_INPUTS client.current_app_id = LIVE_TV_APP_ID client.channels = [CHANNEL_1, CHANNEL_2] diff --git a/tests/components/webostv/const.py b/tests/components/webostv/const.py new file mode 100644 index 00000000000..eca38837d8e --- /dev/null +++ b/tests/components/webostv/const.py @@ -0,0 +1,36 @@ +"""Constants for LG webOS Smart TV tests.""" +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.webostv.const import LIVE_TV_APP_ID + +FAKE_UUID = "some-fake-uuid" +TV_NAME = "fake_webos" +ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}" +HOST = "1.2.3.4" +CLIENT_KEY = "some-secret" +MOCK_CLIENT_KEYS = {HOST: CLIENT_KEY} +MOCK_JSON = '{"1.2.3.4": "some-secret"}' + +CHANNEL_1 = { + "channelNumber": "1", + "channelName": "Channel 1", + "channelId": "ch1id", +} +CHANNEL_2 = { + "channelNumber": "20", + "channelName": "Channel Name 2", + "channelId": "ch2id", +} + +MOCK_APPS = { + LIVE_TV_APP_ID: { + "title": "Live TV", + "id": LIVE_TV_APP_ID, + "largeIcon": "large-icon", + "icon": "icon", + }, +} + +MOCK_INPUTS = { + "in1": {"label": "Input01", "id": "in1", "appId": "app0"}, + "in2": {"label": "Input02", "id": "in2", "appId": "app1"}, +} diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 34e6d96bfdd..b2b20677513 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN +from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV_APP_ID from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import ( CONF_CLIENT_SECRET, @@ -24,7 +24,8 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import CLIENT_KEY, FAKE_UUID, HOST, TV_NAME, setup_webostv +from . import setup_webostv +from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME MOCK_YAML_CONFIG = { CONF_HOST: HOST, @@ -149,8 +150,27 @@ async def test_form(hass, client): assert result["title"] == TV_NAME -async def test_options_flow(hass, client): - """Test options config flow.""" +@pytest.mark.parametrize( + "apps, inputs", + [ + # Live TV in apps (default) + (MOCK_APPS, MOCK_INPUTS), + # Live TV in inputs + ( + {}, + { + **MOCK_INPUTS, + "livetv": {"label": "Live TV", "id": "livetv", "appId": LIVE_TV_APP_ID}, + }, + ), + # Live TV not found + ({}, MOCK_INPUTS), + ], +) +async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): + """Test options config flow Live TV found in apps.""" + client.apps = apps + client.inputs = inputs entry = await setup_webostv(hass) result = await hass.config_entries.options.async_init(entry.entry_id) @@ -161,20 +181,24 @@ async def test_options_flow(hass, client): result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_SOURCES: ["Input01", "Input02"]}, + user_input={CONF_SOURCES: ["Live TV", "Input01", "Input02"]}, ) await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"][CONF_SOURCES] == ["Input01", "Input02"] + assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] + + +async def test_options_flow_cannot_retrieve(hass, client): + """Test options config flow cannot retrieve sources.""" + entry = await setup_webostv(hass) client.connect = Mock(side_effect=ConnectionRefusedError()) - result3 = await hass.config_entries.options.async_init(entry.entry_id) - + result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM - assert result3["errors"] == {"base": "cannot_retrieve"} + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_retrieve"} async def test_form_cannot_connect(hass, client): diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index ef7c86327a8..fb8512d56f1 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -11,7 +11,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import async_get as get_dev_reg from homeassistant.setup import async_setup_component -from . import ENTITY_ID, FAKE_UUID, setup_webostv +from . import setup_webostv +from .const import ENTITY_ID, FAKE_UUID from tests.common import MockConfigEntry, async_get_device_automations diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index eeb1e2fa0ee..8729576d869 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -8,11 +8,11 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.webostv import DOMAIN from . import ( - MOCK_JSON, create_memory_sqlite_engine, is_entity_unique_id_updated, setup_legacy_component, ) +from .const import MOCK_JSON async def test_missing_keys_file_abort(hass, client, caplog): diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index a4071c741e5..c249b491d9a 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -64,7 +64,8 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component from homeassistant.util import dt -from . import CHANNEL_2, ENTITY_ID, TV_NAME, setup_webostv +from . import setup_webostv +from .const import CHANNEL_2, ENTITY_ID, TV_NAME from tests.common import async_fire_time_changed diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index 315ae6ac919..a5188545737 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -9,7 +9,8 @@ from homeassistant.components.webostv import DOMAIN from homeassistant.const import CONF_ICON, CONF_SERVICE_DATA from homeassistant.setup import async_setup_component -from . import TV_NAME, setup_webostv +from . import setup_webostv +from .const import TV_NAME ICON_PATH = "/some/path" MESSAGE = "one, two, testing, testing" diff --git a/tests/components/webostv/test_trigger.py b/tests/components/webostv/test_trigger.py index f2fabe58ede..cbc72638ad9 100644 --- a/tests/components/webostv/test_trigger.py +++ b/tests/components/webostv/test_trigger.py @@ -7,7 +7,8 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.helpers.device_registry import async_get as get_dev_reg from homeassistant.setup import async_setup_component -from . import ENTITY_ID, FAKE_UUID, setup_webostv +from . import setup_webostv +from .const import ENTITY_ID, FAKE_UUID from tests.common import MockEntity, MockEntityPlatform From 5174e68b16f475baf3b8f3c6dd58fcb0d20749d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:09:56 -0600 Subject: [PATCH 073/298] Fix powerwall login retry when hitting rate limit (#65245) --- .../components/powerwall/__init__.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 5fb974fa5c7..fccd8979631 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -5,6 +5,7 @@ import logging import requests from tesla_powerwall import ( AccessDeniedError, + APIError, MissingAttributeError, Powerwall, PowerwallUnreachableError, @@ -131,7 +132,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: power_wall = Powerwall(ip_address, http_session=http_session) runtime_data[POWERWALL_OBJECT] = power_wall runtime_data[POWERWALL_HTTP_SESSION] = http_session - power_wall.login("", password) + power_wall.login(password) + + async def _async_login_and_retry_update_data(): + """Retry the update after a failed login.""" + nonlocal login_failed_count + # If the session expired, recreate, relogin, and try again + _LOGGER.debug("Retrying login and updating data") + try: + await hass.async_add_executor_job(_recreate_powerwall_login) + data = await _async_update_powerwall_data(hass, entry, power_wall) + except AccessDeniedError as err: + login_failed_count += 1 + if login_failed_count == MAX_LOGIN_FAILURES: + raise ConfigEntryAuthFailed from err + raise UpdateFailed( + f"Login attempt {login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry: {err}" + ) from err + except APIError as err: + raise UpdateFailed(f"Updated failed due to {err}, will retry") from err + else: + login_failed_count = 0 + return data async def async_update_data(): """Fetch data from API endpoint.""" @@ -147,18 +169,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AccessDeniedError as err: if password is None: raise ConfigEntryAuthFailed from err - - # If the session expired, recreate, relogin, and try again - try: - await hass.async_add_executor_job(_recreate_powerwall_login) - return await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError as ex: - login_failed_count += 1 - if login_failed_count == MAX_LOGIN_FAILURES: - raise ConfigEntryAuthFailed from ex - raise UpdateFailed( - f"Login attempt {login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry" - ) from ex + return await _async_login_and_retry_update_data() + except APIError as err: + raise UpdateFailed(f"Updated failed due to {err}, will retry") from err else: login_failed_count = 0 return data From ffe262abce5634e0bd36ae0134f51b019e982d00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:17:19 -0600 Subject: [PATCH 074/298] Fix flux_led not generating unique ids when discovery fails (#65250) --- homeassistant/components/flux_led/__init__.py | 29 +++++++- homeassistant/components/flux_led/button.py | 4 +- homeassistant/components/flux_led/entity.py | 51 ++++++++------ homeassistant/components/flux_led/light.py | 6 +- homeassistant/components/flux_led/number.py | 18 ++--- homeassistant/components/flux_led/select.py | 22 +++--- homeassistant/components/flux_led/sensor.py | 2 +- homeassistant/components/flux_led/switch.py | 10 +-- tests/components/flux_led/test_init.py | 51 +++++++++++++- tests/components/flux_led/test_light.py | 6 +- tests/components/flux_led/test_number.py | 19 +++++- tests/components/flux_led/test_select.py | 42 ++++++++++++ tests/components/flux_led/test_switch.py | 68 ++++++++++++++++++- 13 files changed, 268 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index efab893be42..ff1962aed1b 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import ( async_track_time_change, @@ -88,6 +88,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate entities when the mac address gets discovered.""" + unique_id = entry.unique_id + if not unique_id: + return + entry_id = entry.entry_id + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: + # Old format {entry_id}..... + # New format {unique_id}.... + entity_unique_id = entity_entry.unique_id + if not entity_unique_id.startswith(entry_id): + return None + new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + _LOGGER.info( + "Migrating unique_id from [%s] to [%s]", + entity_unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flux LED/MagicLight from a config entry.""" host = entry.data[CONF_HOST] @@ -135,6 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # is either missing or we have verified it matches async_update_entry_from_discovery(hass, entry, discovery, device.model_num) + await _async_migrate_unique_ids(hass, entry) + coordinator = FluxLedUpdateCoordinator(hass, device, entry) hass.data[DOMAIN][entry.entry_id] = coordinator platforms = PLATFORMS_BY_TYPE[device.device_type] diff --git a/homeassistant/components/flux_led/button.py b/homeassistant/components/flux_led/button.py index 3f74090f075..fcd4ecc3adc 100644 --- a/homeassistant/components/flux_led/button.py +++ b/homeassistant/components/flux_led/button.py @@ -64,8 +64,8 @@ class FluxButton(FluxBaseEntity, ButtonEntity): self.entity_description = description super().__init__(device, entry) self._attr_name = f"{entry.data[CONF_NAME]} {description.name}" - if entry.unique_id: - self._attr_unique_id = f"{entry.unique_id}_{description.key}" + base_unique_id = entry.unique_id or entry.entry_id + self._attr_unique_id = f"{base_unique_id}_{description.key}" async def async_press(self) -> None: """Send out a command.""" diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index c06070002d4..5946ab817de 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -7,19 +7,28 @@ from typing import Any from flux_led.aiodevice import AIOWifiLedBulb from homeassistant import config_entries -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + ATTR_CONNECTIONS, + ATTR_HW_VERSION, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, + CONF_NAME, +) from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_MINOR_VERSION, CONF_MODEL, SIGNAL_STATE_UPDATED +from .const import CONF_MINOR_VERSION, CONF_MODEL, DOMAIN, SIGNAL_STATE_UPDATED from .coordinator import FluxLedUpdateCoordinator def _async_device_info( - unique_id: str, device: AIOWifiLedBulb, entry: config_entries.ConfigEntry + device: AIOWifiLedBulb, entry: config_entries.ConfigEntry ) -> DeviceInfo: version_num = device.version_num if minor_version := entry.data.get(CONF_MINOR_VERSION): @@ -27,14 +36,18 @@ def _async_device_info( sw_version_str = f"{sw_version:0.2f}" else: sw_version_str = str(device.version_num) - return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, unique_id)}, - manufacturer="Zengge", - model=device.model, - name=entry.data[CONF_NAME], - sw_version=sw_version_str, - hw_version=entry.data.get(CONF_MODEL), - ) + device_info: DeviceInfo = { + ATTR_IDENTIFIERS: {(DOMAIN, entry.entry_id)}, + ATTR_MANUFACTURER: "Zengge", + ATTR_MODEL: device.model, + ATTR_NAME: entry.data[CONF_NAME], + ATTR_SW_VERSION: sw_version_str, + } + if hw_model := entry.data.get(CONF_MODEL): + device_info[ATTR_HW_VERSION] = hw_model + if entry.unique_id: + device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} + return device_info class FluxBaseEntity(Entity): @@ -50,10 +63,7 @@ class FluxBaseEntity(Entity): """Initialize the light.""" self._device: AIOWifiLedBulb = device self.entry = entry - if entry.unique_id: - self._attr_device_info = _async_device_info( - entry.unique_id, self._device, entry - ) + self._attr_device_info = _async_device_info(self._device, entry) class FluxEntity(CoordinatorEntity): @@ -64,7 +74,7 @@ class FluxEntity(CoordinatorEntity): def __init__( self, coordinator: FluxLedUpdateCoordinator, - unique_id: str | None, + base_unique_id: str, name: str, key: str | None, ) -> None: @@ -74,13 +84,10 @@ class FluxEntity(CoordinatorEntity): self._responding = True self._attr_name = name if key: - self._attr_unique_id = f"{unique_id}_{key}" + self._attr_unique_id = f"{base_unique_id}_{key}" else: - self._attr_unique_id = unique_id - if unique_id: - self._attr_device_info = _async_device_info( - unique_id, self._device, coordinator.entry - ) + self._attr_unique_id = base_unique_id + self._attr_device_info = _async_device_info(self._device, coordinator.entry) async def _async_ensure_device_on(self) -> None: """Turn the device on if it needs to be turned on before a command.""" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 85b74616b32..4534c45e228 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -177,7 +177,7 @@ async def async_setup_entry( [ FluxLight( coordinator, - entry.unique_id, + entry.unique_id or entry.entry_id, entry.data[CONF_NAME], list(custom_effect_colors), options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED), @@ -195,14 +195,14 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): def __init__( self, coordinator: FluxLedUpdateCoordinator, - unique_id: str | None, + base_unique_id: str, name: str, custom_effect_colors: list[tuple[int, int, int]], custom_effect_speed_pct: int, custom_effect_transition: str, ) -> None: """Initialize the light.""" - super().__init__(coordinator, unique_id, name, None) + super().__init__(coordinator, base_unique_id, name, None) self._attr_min_mireds = color_temperature_kelvin_to_mired(self._device.max_temp) self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp) self._attr_supported_color_modes = _hass_color_modes(self._device) diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 7c607219901..d7fad9cf0e6 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -51,26 +51,28 @@ async def async_setup_entry( | FluxMusicSegmentsNumber ] = [] name = entry.data[CONF_NAME] - unique_id = entry.unique_id + base_unique_id = entry.unique_id or entry.entry_id if device.pixels_per_segment is not None: entities.append( FluxPixelsPerSegmentNumber( coordinator, - unique_id, + base_unique_id, f"{name} Pixels Per Segment", "pixels_per_segment", ) ) if device.segments is not None: entities.append( - FluxSegmentsNumber(coordinator, unique_id, f"{name} Segments", "segments") + FluxSegmentsNumber( + coordinator, base_unique_id, f"{name} Segments", "segments" + ) ) if device.music_pixels_per_segment is not None: entities.append( FluxMusicPixelsPerSegmentNumber( coordinator, - unique_id, + base_unique_id, f"{name} Music Pixels Per Segment", "music_pixels_per_segment", ) @@ -78,12 +80,12 @@ async def async_setup_entry( if device.music_segments is not None: entities.append( FluxMusicSegmentsNumber( - coordinator, unique_id, f"{name} Music Segments", "music_segments" + coordinator, base_unique_id, f"{name} Music Segments", "music_segments" ) ) if device.effect_list and device.effect_list != [EFFECT_RANDOM]: entities.append( - FluxSpeedNumber(coordinator, unique_id, f"{name} Effect Speed", None) + FluxSpeedNumber(coordinator, base_unique_id, f"{name} Effect Speed", None) ) if entities: @@ -131,12 +133,12 @@ class FluxConfigNumber(FluxEntity, CoordinatorEntity, NumberEntity): def __init__( self, coordinator: FluxLedUpdateCoordinator, - unique_id: str | None, + base_unique_id: str, name: str, key: str | None, ) -> None: """Initialize the flux number.""" - super().__init__(coordinator, unique_id, name, key) + super().__init__(coordinator, base_unique_id, name, key) self._debouncer: Debouncer | None = None self._pending_value: int | None = None diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index 701465da036..3b78baa782b 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -54,28 +54,28 @@ async def async_setup_entry( | FluxWhiteChannelSelect ] = [] name = entry.data[CONF_NAME] - unique_id = entry.unique_id + base_unique_id = entry.unique_id or entry.entry_id if device.device_type == DeviceType.Switch: entities.append(FluxPowerStateSelect(coordinator.device, entry)) if device.operating_modes: entities.append( FluxOperatingModesSelect( - coordinator, unique_id, f"{name} Operating Mode", "operating_mode" + coordinator, base_unique_id, f"{name} Operating Mode", "operating_mode" ) ) if device.wirings: entities.append( - FluxWiringsSelect(coordinator, unique_id, f"{name} Wiring", "wiring") + FluxWiringsSelect(coordinator, base_unique_id, f"{name} Wiring", "wiring") ) if device.ic_types: entities.append( - FluxICTypeSelect(coordinator, unique_id, f"{name} IC Type", "ic_type") + FluxICTypeSelect(coordinator, base_unique_id, f"{name} IC Type", "ic_type") ) if device.remote_config: entities.append( FluxRemoteConfigSelect( - coordinator, unique_id, f"{name} Remote Config", "remote_config" + coordinator, base_unique_id, f"{name} Remote Config", "remote_config" ) ) if FLUX_COLOR_MODE_RGBW in device.color_modes: @@ -111,8 +111,8 @@ class FluxPowerStateSelect(FluxConfigAtStartSelect, SelectEntity): """Initialize the power state select.""" super().__init__(device, entry) self._attr_name = f"{entry.data[CONF_NAME]} Power Restored" - if entry.unique_id: - self._attr_unique_id = f"{entry.unique_id}_power_restored" + base_unique_id = entry.unique_id or entry.entry_id + self._attr_unique_id = f"{base_unique_id}_power_restored" self._async_set_current_option_from_device() @callback @@ -201,12 +201,12 @@ class FluxRemoteConfigSelect(FluxConfigSelect): def __init__( self, coordinator: FluxLedUpdateCoordinator, - unique_id: str | None, + base_unique_id: str, name: str, key: str, ) -> None: """Initialize the remote config type select.""" - super().__init__(coordinator, unique_id, name, key) + super().__init__(coordinator, base_unique_id, name, key) assert self._device.remote_config is not None self._name_to_state = { _human_readable_option(option.name): option for option in RemoteConfig @@ -238,8 +238,8 @@ class FluxWhiteChannelSelect(FluxConfigAtStartSelect): """Initialize the white channel select.""" super().__init__(device, entry) self._attr_name = f"{entry.data[CONF_NAME]} White Channel" - if entry.unique_id: - self._attr_unique_id = f"{entry.unique_id}_white_channel" + base_unique_id = entry.unique_id or entry.entry_id + self._attr_unique_id = f"{base_unique_id}_white_channel" @property def current_option(self) -> str | None: diff --git a/homeassistant/components/flux_led/sensor.py b/homeassistant/components/flux_led/sensor.py index 6d67ced1fe2..18d1aac5506 100644 --- a/homeassistant/components/flux_led/sensor.py +++ b/homeassistant/components/flux_led/sensor.py @@ -25,7 +25,7 @@ async def async_setup_entry( [ FluxPairedRemotes( coordinator, - entry.unique_id, + entry.unique_id or entry.entry_id, f"{entry.data[CONF_NAME]} Paired Remotes", "paired_remotes", ) diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index f8de86d3340..ee004fc2250 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -34,18 +34,18 @@ async def async_setup_entry( """Set up the Flux lights.""" coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = [] - unique_id = entry.unique_id + base_unique_id = entry.unique_id or entry.entry_id name = entry.data[CONF_NAME] if coordinator.device.device_type == DeviceType.Switch: - entities.append(FluxSwitch(coordinator, unique_id, name, None)) + entities.append(FluxSwitch(coordinator, base_unique_id, name, None)) if entry.data.get(CONF_REMOTE_ACCESS_HOST): entities.append(FluxRemoteAccessSwitch(coordinator.device, entry)) if coordinator.device.microphone: entities.append( - FluxMusicSwitch(coordinator, unique_id, f"{name} Music", "music") + FluxMusicSwitch(coordinator, base_unique_id, f"{name} Music", "music") ) if entities: @@ -74,8 +74,8 @@ class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity): """Initialize the light.""" super().__init__(device, entry) self._attr_name = f"{entry.data[CONF_NAME]} Remote Access" - if entry.unique_id: - self._attr_unique_id = f"{entry.unique_id}_remote_access" + base_unique_id = entry.unique_id or entry.entry_id + self._attr_unique_id = f"{base_unique_id}_remote_access" async def async_turn_on(self, **kwargs: Any) -> None: """Turn the remote access on.""" diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 7981f2cef11..de655c2e6ad 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -7,10 +7,16 @@ from unittest.mock import patch import pytest from homeassistant.components import flux_led -from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.components.flux_led.const import ( + CONF_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -156,3 +162,46 @@ async def test_time_sync_startup_and_next_day(hass: HomeAssistant) -> None: async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() assert len(bulb.async_set_time.mock_calls) == 2 + + +async def test_unique_id_migrate_when_mac_discovered(hass: HomeAssistant) -> None: + """Test unique id migrated when mac discovered.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + assert not config_entry.unique_id + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get("light.bulb_rgbcw_ddeeff").unique_id + == config_entry.entry_id + ) + assert ( + entity_registry.async_get("switch.bulb_rgbcw_ddeeff_remote_access").unique_id + == f"{config_entry.entry_id}_remote_access" + ) + + with _patch_discovery(), _patch_wifibulb(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get("light.bulb_rgbcw_ddeeff").unique_id + == config_entry.unique_id + ) + assert ( + entity_registry.async_get("switch.bulb_rgbcw_ddeeff_remote_access").unique_id + == f"{config_entry.unique_id}_remote_access" + ) diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 83a76311e8a..67603544c5d 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -137,8 +137,8 @@ async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: assert state.state == STATE_ON -async def test_light_no_unique_id(hass: HomeAssistant) -> None: - """Test a light without a unique id.""" +async def test_light_mac_address_not_found(hass: HomeAssistant) -> None: + """Test a light when we cannot discover the mac address.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} ) @@ -150,7 +150,7 @@ async def test_light_no_unique_id(hass: HomeAssistant) -> None: entity_id = "light.bulb_rgbcw_ddeeff" entity_registry = er.async_get(hass) - assert entity_registry.async_get(entity_id) is None + assert entity_registry.async_get(entity_id).unique_id == config_entry.entry_id state = hass.states.get(entity_id) assert state.state == STATE_ON diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index a4b23f47fcc..d0d71cacbe1 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -41,7 +41,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_number_unique_id(hass: HomeAssistant) -> None: +async def test_effects_speed_unique_id(hass: HomeAssistant) -> None: """Test a number unique id.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -59,6 +59,23 @@ async def test_number_unique_id(hass: HomeAssistant) -> None: assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS +async def test_effects_speed_unique_id_no_discovery(hass: HomeAssistant) -> None: + """Test a number unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.bulb_rgbcw_ddeeff_effect_speed" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == config_entry.entry_id + + async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: """Test an rgb light with an effect.""" config_entry = MockConfigEntry( diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py index 6df276e5011..b2a88b00fe0 100644 --- a/tests/components/flux_led/test_select.py +++ b/tests/components/flux_led/test_select.py @@ -14,6 +14,7 @@ from homeassistant.components.flux_led.const import CONF_WHITE_CHANNEL_TYPE, DOM from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from . import ( @@ -67,6 +68,47 @@ async def test_switch_power_restore_state(hass: HomeAssistant) -> None: ) +async def test_power_restored_unique_id(hass: HomeAssistant) -> None: + """Test a select unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + switch = _mocked_switch() + with _patch_discovery(), _patch_wifibulb(device=switch): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.bulb_rgbcw_ddeeff_power_restored" + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get(entity_id).unique_id + == f"{MAC_ADDRESS}_power_restored" + ) + + +async def test_power_restored_unique_id_no_discovery(hass: HomeAssistant) -> None: + """Test a select unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + ) + config_entry.add_to_hass(hass) + switch = _mocked_switch() + with _patch_discovery(no_device=True), _patch_wifibulb(device=switch): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.bulb_rgbcw_ddeeff_power_restored" + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get(entity_id).unique_id + == f"{config_entry.entry_id}_power_restored" + ) + + async def test_select_addressable_strip_config(hass: HomeAssistant) -> None: """Test selecting addressable strip configs.""" config_entry = MockConfigEntry( diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index ce2855a53ed..cb0034f8d36 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -2,7 +2,12 @@ from flux_led.const import MODE_MUSIC from homeassistant.components import flux_led -from homeassistant.components.flux_led.const import CONF_REMOTE_ACCESS_ENABLED, DOMAIN +from homeassistant.components.flux_led.const import ( + CONF_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT, + DOMAIN, +) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -12,6 +17,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from . import ( @@ -65,11 +71,69 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: assert hass.states.get(entity_id).state == STATE_ON +async def test_remote_access_unique_id(hass: HomeAssistant) -> None: + """Test a remote access switch unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.bulb_rgbcw_ddeeff_remote_access" + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get(entity_id).unique_id == f"{MAC_ADDRESS}_remote_access" + ) + + +async def test_effects_speed_unique_id_no_discovery(hass: HomeAssistant) -> None: + """Test a remote access switch unique id when discovery fails.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.bulb_rgbcw_ddeeff_remote_access" + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get(entity_id).unique_id + == f"{config_entry.entry_id}_remote_access" + ) + + async def test_remote_access_on_off(hass: HomeAssistant) -> None: """Test enable/disable remote access.""" config_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, unique_id=MAC_ADDRESS, ) config_entry.add_to_hass(hass) From ac8a1248f9805d0efdad4f3463aad048215d7a35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 11:08:37 -0600 Subject: [PATCH 075/298] Fix debugpy blocking the event loop at startup (#65252) --- homeassistant/components/debugpy/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index 21cfeb15a80..1dc0f525c4d 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -46,7 +46,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Enable asyncio debugging and start the debugger.""" get_running_loop().set_debug(True) - debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) + await hass.async_add_executor_job( + debugpy.listen, (conf[CONF_HOST], conf[CONF_PORT]) + ) if conf[CONF_WAIT]: _LOGGER.warning( From 7e350b834700ff0ad96d572700585fc10df7c447 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 15:19:04 -0600 Subject: [PATCH 076/298] Handle missing attrs in whois results (#65254) * Handle missing attrs in whois results - Some attrs are not set depending on where the domain is registered - Fixes #65164 * Set to unknown instead of do not create * no multi-line lambda --- homeassistant/components/whois/sensor.py | 16 ++++++--- tests/components/whois/conftest.py | 44 +++++++++++++++++++++++- tests/components/whois/test_init.py | 4 +-- tests/components/whois/test_sensor.py | 26 ++++++++++++++ 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 2cbae147a78..0459651e693 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -80,6 +80,13 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: return timestamp +def _fetch_attr_if_exists(domain: Domain, attr: str) -> str | None: + """Fetch an attribute if it exists and is truthy or return None.""" + if hasattr(domain, attr) and (value := getattr(domain, attr)): + return cast(str, value) + return None + + SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="admin", @@ -87,7 +94,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:account-star", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: domain.admin if domain.admin else None, + value_fn=lambda domain: _fetch_attr_if_exists(domain, "admin"), ), WhoisSensorEntityDescription( key="creation_date", @@ -123,7 +130,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:account", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: domain.owner if domain.owner else None, + value_fn=lambda domain: _fetch_attr_if_exists(domain, "owner"), ), WhoisSensorEntityDescription( key="registrant", @@ -131,7 +138,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:account-edit", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: domain.registrant if domain.registrant else None, + value_fn=lambda domain: _fetch_attr_if_exists(domain, "registrant"), ), WhoisSensorEntityDescription( key="registrar", @@ -147,7 +154,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: domain.reseller if domain.reseller else None, + value_fn=lambda domain: _fetch_attr_if_exists(domain, "reseller"), ), ) @@ -190,7 +197,6 @@ async def async_setup_entry( ) for description in SENSORS ], - update_before_add=True, ) diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index bbda3b101f5..ef14750356e 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Generator from datetime import datetime -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -71,6 +71,33 @@ def mock_whois() -> Generator[MagicMock, None, None]: yield whois_mock +@pytest.fixture +def mock_whois_missing_some_attrs() -> Generator[Mock, None, None]: + """Return a mocked query that only sets admin.""" + + class LimitedWhoisMock: + """A limited mock of whois_query.""" + + def __init__(self, *args, **kwargs): + """Mock only attributes the library always sets being available.""" + self.creation_date = datetime(2019, 1, 1, 0, 0, 0) + self.dnssec = True + self.expiration_date = datetime(2023, 1, 1, 0, 0, 0) + self.last_updated = datetime( + 2022, 1, 1, 0, 0, 0, tzinfo=dt_util.get_time_zone("Europe/Amsterdam") + ) + self.name = "home-assistant.io" + self.name_servers = ["ns1.example.com", "ns2.example.com"] + self.registrar = "My Registrar" + self.status = "OK" + self.statuses = ["OK"] + + with patch( + "homeassistant.components.whois.whois_query", LimitedWhoisMock + ) as whois_mock: + yield whois_mock + + @pytest.fixture async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_whois: MagicMock @@ -84,6 +111,21 @@ async def init_integration( return mock_config_entry +@pytest.fixture +async def init_integration_missing_some_attrs( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_whois_missing_some_attrs: MagicMock, +) -> MockConfigEntry: + """Set up thewhois integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry + + @pytest.fixture def enable_all_entities() -> Generator[AsyncMock, None, None]: """Test fixture that ensures all entities are enabled in the registry.""" diff --git a/tests/components/whois/test_init.py b/tests/components/whois/test_init.py index 6701d8ed20c..3cd9efc801d 100644 --- a/tests/components/whois/test_init.py +++ b/tests/components/whois/test_init.py @@ -30,7 +30,7 @@ async def test_load_unload_config_entry( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED - assert len(mock_whois.mock_calls) == 2 + assert len(mock_whois.mock_calls) == 1 await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -76,5 +76,5 @@ async def test_import_config( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_whois.mock_calls) == 2 + assert len(mock_whois.mock_calls) == 1 assert "the Whois platform in YAML is deprecated" in caplog.text diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index b0e9862eb64..e824522ed09 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -143,6 +143,32 @@ async def test_whois_sensors( assert device_entry.sw_version is None +@pytest.mark.freeze_time("2022-01-01 12:00:00", tz_offset=0) +async def test_whois_sensors_missing_some_attrs( + hass: HomeAssistant, + enable_all_entities: AsyncMock, + init_integration_missing_some_attrs: MockConfigEntry, +) -> None: + """Test the Whois sensors with owner and reseller missing.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.home_assistant_io_last_updated") + entry = entity_registry.async_get("sensor.home_assistant_io_last_updated") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_last_updated" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "2021-12-31T23:00:00+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + assert hass.states.get("sensor.home_assistant_io_owner").state == STATE_UNKNOWN + assert hass.states.get("sensor.home_assistant_io_reseller").state == STATE_UNKNOWN + assert hass.states.get("sensor.home_assistant_io_registrant").state == STATE_UNKNOWN + assert hass.states.get("sensor.home_assistant_io_admin").state == STATE_UNKNOWN + + @pytest.mark.parametrize( "entity_id", ( From 8bdee9cb1c5b3160fd92e490b97e4e536e015893 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:19:52 -0600 Subject: [PATCH 077/298] Simplify whois value_fn (#65265) --- homeassistant/components/whois/sensor.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 0459651e693..6df920f385c 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -80,13 +80,6 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: return timestamp -def _fetch_attr_if_exists(domain: Domain, attr: str) -> str | None: - """Fetch an attribute if it exists and is truthy or return None.""" - if hasattr(domain, attr) and (value := getattr(domain, attr)): - return cast(str, value) - return None - - SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="admin", @@ -94,7 +87,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:account-star", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: _fetch_attr_if_exists(domain, "admin"), + value_fn=lambda domain: getattr(domain, "admin", None), ), WhoisSensorEntityDescription( key="creation_date", @@ -130,7 +123,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:account", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: _fetch_attr_if_exists(domain, "owner"), + value_fn=lambda domain: getattr(domain, "owner", None), ), WhoisSensorEntityDescription( key="registrant", @@ -138,7 +131,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:account-edit", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: _fetch_attr_if_exists(domain, "registrant"), + value_fn=lambda domain: getattr(domain, "registrant", None), ), WhoisSensorEntityDescription( key="registrar", @@ -154,7 +147,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda domain: _fetch_attr_if_exists(domain, "reseller"), + value_fn=lambda domain: getattr(domain, "reseller", None), ), ) From 252f5f6b354cd5f6f983b393603f90e42e64c983 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 31 Jan 2022 00:04:00 +0200 Subject: [PATCH 078/298] Bump aiowebostv to 0.1.2 (#65267) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 3a2a3025277..1494180dd05 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.1.1", "sqlalchemy==1.4.27"], + "requirements": ["aiowebostv==0.1.2", "sqlalchemy==1.4.27"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{"st": "urn:lge-com:service:webos-second-screen:1"}], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 31e5cc0e5cc..d2db9851e93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -278,7 +278,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.1 +aiowebostv==0.1.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 929baafec7e..601d1236ed3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -213,7 +213,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.1 +aiowebostv==0.1.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 6b6bd381fd63825ac996b6bcd0df9164c04eb0fe Mon Sep 17 00:00:00 2001 From: Brynley McDonald Date: Mon, 31 Jan 2022 17:21:15 +1300 Subject: [PATCH 079/298] Fix flick_electric auth failures (#65274) --- .../components/flick_electric/__init__.py | 18 ++++++++++++++---- .../components/flick_electric/const.py | 1 - .../components/flick_electric/sensor.py | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index c29a476ca55..54eaf5a6917 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -1,7 +1,9 @@ """The Flick Electric integration.""" from datetime import datetime as dt +import logging +import jwt from pyflick import FlickAPI from pyflick.authentication import AbstractFlickAuth from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET @@ -18,7 +20,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import CONF_TOKEN_EXPIRES_IN, CONF_TOKEN_EXPIRY, DOMAIN +from .const import CONF_TOKEN_EXPIRY, DOMAIN + +_LOGGER = logging.getLogger(__name__) CONF_ID_TOKEN = "id_token" @@ -69,6 +73,8 @@ class HassFlickAuth(AbstractFlickAuth): return self._entry.data[CONF_ACCESS_TOKEN] async def _update_token(self): + _LOGGER.debug("Fetching new access token") + token = await self.get_new_token( username=self._entry.data[CONF_USERNAME], password=self._entry.data[CONF_PASSWORD], @@ -78,15 +84,19 @@ class HassFlickAuth(AbstractFlickAuth): ), ) - # Reduce expiry by an hour to avoid API being called after expiry - expiry = dt.now().timestamp() + int(token[CONF_TOKEN_EXPIRES_IN] - 3600) + _LOGGER.debug("New token: %s", token) + + # Flick will send the same token, but expiry is relative - so grab it from the token + token_decoded = jwt.decode( + token[CONF_ID_TOKEN], options={"verify_signature": False} + ) self._hass.config_entries.async_update_entry( self._entry, data={ **self._entry.data, CONF_ACCESS_TOKEN: token, - CONF_TOKEN_EXPIRY: expiry, + CONF_TOKEN_EXPIRY: token_decoded["exp"], }, ) diff --git a/homeassistant/components/flick_electric/const.py b/homeassistant/components/flick_electric/const.py index e8365f37411..de1942096b5 100644 --- a/homeassistant/components/flick_electric/const.py +++ b/homeassistant/components/flick_electric/const.py @@ -2,7 +2,6 @@ DOMAIN = "flick_electric" -CONF_TOKEN_EXPIRES_IN = "expires_in" CONF_TOKEN_EXPIRY = "expires" ATTR_START_AT = "start_at" diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index d6a1bd59d8e..92bc81b5aa0 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -15,8 +15,6 @@ from homeassistant.util.dt import utcnow from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT, DOMAIN _LOGGER = logging.getLogger(__name__) -_AUTH_URL = "https://api.flick.energy/identity/oauth/token" -_RESOURCE = "https://api.flick.energy/customer/mobile_provider/price" SCAN_INTERVAL = timedelta(minutes=5) @@ -71,6 +69,8 @@ class FlickPricingSensor(SensorEntity): async with async_timeout.timeout(60): self._price = await self._api.getPricing() + _LOGGER.debug("Pricing data: %s", self._price) + self._attributes[ATTR_START_AT] = self._price.start_at self._attributes[ATTR_END_AT] = self._price.end_at for component in self._price.components: From 5d7aefa0b43aca788045adbc1ebba43fd418588e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 31 Jan 2022 05:12:44 +0100 Subject: [PATCH 080/298] Update xknx to 0.19.1 (#65275) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 7 +------ tests/components/knx/test_config_flow.py | 10 +++++----- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6e229edb893..9889f39ab35 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.19.0" + "xknx==0.19.1" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index d2db9851e93..a8709e3c6ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2496,7 +2496,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.19.0 +xknx==0.19.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 601d1236ed3..560d5e36e31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1527,7 +1527,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.19.0 +xknx==0.19.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index cd15d77629c..71a86f1e397 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -13,7 +13,7 @@ from xknx.telegram import Telegram, TelegramDirection from xknx.telegram.address import GroupAddress, IndividualAddress from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite -from homeassistant.components.knx import ConnectionSchema, KNXModule +from homeassistant.components.knx import ConnectionSchema from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, @@ -40,11 +40,6 @@ class KNXTestKit: # telegrams to an InternalGroupAddress won't be queued here self._outgoing_telegrams: asyncio.Queue = asyncio.Queue() - @property - def knx_module(self) -> KNXModule: - """Get the KNX module.""" - return self.hass.data[KNX_DOMAIN] - def assert_state(self, entity_id: str, state: str, **attributes) -> None: """Assert the state of an entity.""" test_state = self.hass.states.get(entity_id) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index d6c2ec98b20..83b5a9988c7 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -39,11 +39,11 @@ def _gateway_descriptor( ) -> GatewayDescriptor: """Get mock gw descriptor.""" return GatewayDescriptor( - "Test", - ip, - port, - "eth0", - "127.0.0.1", + name="Test", + ip_addr=ip, + port=port, + local_interface="eth0", + local_ip="127.0.0.1", supports_routing=True, supports_tunnelling=True, supports_tunnelling_tcp=supports_tunnelling_tcp, From ef143b5eb243f99d3b89ae33cde2242e4d85247e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Jan 2022 20:28:42 -0800 Subject: [PATCH 081/298] Bumped version to 2022.2.0b4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6f0f4011810..842268f7e0d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index e0ec1326e20..5f6efe18517 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.0b3 +version = 2022.2.0b4 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From cdcbb87d9760597cc7b3b9056d55fe1553ee6bd3 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 31 Jan 2022 03:09:07 +0100 Subject: [PATCH 082/298] Bump python-kasa to 0.4.1 for tplink integration (#64123) Co-authored-by: J. Nick Koston --- homeassistant/components/tplink/__init__.py | 2 +- homeassistant/components/tplink/light.py | 9 +++++---- homeassistant/components/tplink/manifest.json | 2 +- homeassistant/components/tplink/switch.py | 9 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index c2ada4190b3..e6b4c4aceab 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device: SmartDevice = hass_data[entry.entry_id].device if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass_data.pop(entry.entry_id) - await device.protocol.close() + await device.protocol.close() # type: ignore return unload_ok diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index ad423e84fa5..6efabe537f7 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -2,9 +2,9 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -from kasa import SmartDevice +from kasa import SmartBulb from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -41,7 +41,7 @@ async def async_setup_entry( ) -> None: """Set up switches.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - device = coordinator.device + device = cast(SmartBulb, coordinator.device) if device.is_bulb or device.is_light_strip or device.is_dimmer: async_add_entities([TPLinkSmartBulb(device, coordinator)]) @@ -50,10 +50,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Representation of a TPLink Smart Bulb.""" coordinator: TPLinkDataUpdateCoordinator + device: SmartBulb def __init__( self, - device: SmartDevice, + device: SmartBulb, coordinator: TPLinkDataUpdateCoordinator, ) -> None: """Initialize the switch.""" diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 2da05abc35e..5e0b98c4ddb 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -3,7 +3,7 @@ "name": "TP-Link Kasa Smart", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", - "requirements": ["python-kasa==0.4.0"], + "requirements": ["python-kasa==0.4.1"], "codeowners": ["@rytilahti", "@thegardenmonkey"], "dependencies": ["network"], "quality_scale": "platinum", diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index eb1094b7675..823d37267d6 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -2,9 +2,9 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -from kasa import SmartDevice +from kasa import SmartDevice, SmartPlug from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -27,7 +27,7 @@ async def async_setup_entry( ) -> None: """Set up switches.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - device = coordinator.device + device = cast(SmartPlug, coordinator.device) if not device.is_plug and not device.is_strip: return entities: list = [] @@ -48,11 +48,12 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of switch for the LED of a TPLink Smart Plug.""" coordinator: TPLinkDataUpdateCoordinator + device: SmartPlug _attr_entity_category = EntityCategory.CONFIG def __init__( - self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator + self, device: SmartPlug, coordinator: TPLinkDataUpdateCoordinator ) -> None: """Initialize the LED switch.""" super().__init__(device, coordinator) diff --git a/requirements_all.txt b/requirements_all.txt index a8709e3c6ff..5628e197e2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ python-join-api==0.0.6 python-juicenet==1.0.2 # homeassistant.components.tplink -python-kasa==0.4.0 +python-kasa==0.4.1 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 560d5e36e31..c2cae070fd4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1200,7 +1200,7 @@ python-izone==1.2.3 python-juicenet==1.0.2 # homeassistant.components.tplink -python-kasa==0.4.0 +python-kasa==0.4.1 # homeassistant.components.xiaomi_miio python-miio==0.5.9.2 From 3446c95cd36060445e8f42284b51136a637b5d57 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 31 Jan 2022 14:51:39 +1000 Subject: [PATCH 083/298] Add diagnostics to Advantage Air (#65006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- .coveragerc | 1 + .../components/advantage_air/diagnostics.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 homeassistant/components/advantage_air/diagnostics.py diff --git a/.coveragerc b/.coveragerc index f0af68a9cda..c1b2811b63c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -27,6 +27,7 @@ omit = homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* + homeassistant/components/advantage_air/diagnostics.py homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/* homeassistant/components/agent_dvr/alarm_control_panel.py diff --git a/homeassistant/components/advantage_air/diagnostics.py b/homeassistant/components/advantage_air/diagnostics.py new file mode 100644 index 00000000000..27eaef09b43 --- /dev/null +++ b/homeassistant/components/advantage_air/diagnostics.py @@ -0,0 +1,25 @@ +"""Provides diagnostics for Advantage Air.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN + +TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data + + # Return only the relevant children + return { + "aircons": data["aircons"], + "system": async_redact_data(data["system"], TO_REDACT), + } From 2eef05eb849c909ad96913e796d66fbc3988f2e6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 31 Jan 2022 12:21:21 -0600 Subject: [PATCH 084/298] Send notification to alert of Sonos networking issues (#65084) * Send notification to alert of Sonos networking issues * Add links to documentation --- homeassistant/components/sonos/entity.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 74d310d40ec..53768431a1d 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -9,6 +9,7 @@ import soco.config as soco_config from soco.core import SoCo from soco.exceptions import SoCoException +from homeassistant.components import persistent_notification import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity @@ -22,6 +23,8 @@ from .const import ( ) from .speaker import SonosSpeaker +SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements" + _LOGGER = logging.getLogger(__name__) @@ -70,10 +73,15 @@ class SonosEntity(Entity): listener_msg = f"{self.speaker.subscription_address} (advertising as {soco_config.EVENT_ADVERTISE_IP})" else: listener_msg = self.speaker.subscription_address - _LOGGER.warning( - "%s cannot reach %s, falling back to polling, functionality may be limited", - self.speaker.zone_name, - listener_msg, + message = f"{self.speaker.zone_name} cannot reach {listener_msg}, falling back to polling, functionality may be limited" + log_link_msg = f", see {SUB_FAIL_URL} for more details" + notification_link_msg = f'.\n\nSee Sonos documentation for more details.' + _LOGGER.warning(message + log_link_msg) + persistent_notification.async_create( + self.hass, + message + notification_link_msg, + "Sonos networking issue", + "sonos_subscriptions_failed", ) self.speaker.subscriptions_failed = True await self.speaker.async_unsubscribe() From 73750d8a255ba95b0384b7ff5df035f94c74163a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 31 Jan 2022 10:50:05 +0100 Subject: [PATCH 085/298] Add cast platform for extending Google Cast media_player (#65149) * Add cast platform for extending Google Cast media_player * Update tests * Refactor according to review comments * Add test for playing using a cast platform * Apply suggestions from code review Co-authored-by: jjlawren * Pass cast type instead of a filter function when browsing * Raise on invalid cast platform * Test media browsing Co-authored-by: jjlawren --- homeassistant/components/cast/__init__.py | 60 ++++- homeassistant/components/cast/media_player.py | 67 ++--- homeassistant/components/plex/cast.py | 75 ++++++ tests/components/cast/conftest.py | 10 - tests/components/cast/test_media_player.py | 241 ++++++++++++++---- 5 files changed, 353 insertions(+), 100 deletions(-) create mode 100644 homeassistant/components/plex/cast.py diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 2623de0933e..cb631e17ccd 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,12 +1,21 @@ """Component to embed Google Cast.""" -import logging +from __future__ import annotations +import logging +from typing import Protocol + +from pychromecast import Chromecast import voluptuous as vol +from homeassistant.components.media_player import BrowseMedia from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) from homeassistant.helpers.typing import ConfigType from . import home_assistant_cast @@ -49,9 +58,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" await home_assistant_cast.async_setup_ha_cast(hass, entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) + hass.data[DOMAIN] = {} + await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True +class CastProtocol(Protocol): + """Define the format of cast platforms.""" + + async def async_get_media_browser_root_object( + self, cast_type: str + ) -> list[BrowseMedia]: + """Create a list of root objects for media browsing.""" + + async def async_browse_media( + self, + hass: HomeAssistant, + media_content_type: str, + media_content_id: str, + cast_type: str, + ) -> BrowseMedia | None: + """Browse media. + + Return a BrowseMedia object or None if the media does not belong to this platform. + """ + + async def async_play_media( + self, + hass: HomeAssistant, + cast_entity_id: str, + chromecast: Chromecast, + media_type: str, + media_id: str, + ) -> bool: + """Play media. + + Return True if the media is played by the platform, False if not. + """ + + +async def _register_cast_platform( + hass: HomeAssistant, integration_domain: str, platform: CastProtocol +): + """Register a cast platform.""" + if ( + not hasattr(platform, "async_get_media_browser_root_object") + or not hasattr(platform, "async_browse_media") + or not hasattr(platform, "async_play_media") + ): + raise HomeAssistantError(f"Invalid cast platform {platform}") + hass.data[DOMAIN][integration_domain] = platform + + async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Remove Home Assistant Cast user.""" await home_assistant_cast.async_remove_user(hass, entry) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 6cca4cfa20b..0b7bc5e5748 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -11,7 +11,6 @@ from urllib.parse import quote import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController from pychromecast.controllers.multizone import MultizoneManager -from pychromecast.controllers.plex import PlexController from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED from pychromecast.quick_play import quick_play from pychromecast.socket_client import ( @@ -20,7 +19,7 @@ from pychromecast.socket_client import ( ) import voluptuous as vol -from homeassistant.components import media_source, plex, zeroconf +from homeassistant.components import media_source, zeroconf from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import ( BrowseError, @@ -29,7 +28,6 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_EXTRA, - MEDIA_CLASS_APP, MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -47,8 +45,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.components.plex.const import PLEX_URI_SCHEME -from homeassistant.components.plex.services import lookup_plex_media from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CAST_APP_ID_HOMEASSISTANT_LOVELACE, @@ -463,21 +459,15 @@ class CastDevice(MediaPlayerEntity): async def _async_root_payload(self, content_filter): """Generate root node.""" children = [] - # Add external sources - if "plex" in self.hass.config.components: - children.append( - BrowseMedia( - title="Plex", - media_class=MEDIA_CLASS_APP, - media_content_id="", - media_content_type="plex", - thumbnail="https://brands.home-assistant.io/_/plex/logo.png", - can_play=False, - can_expand=True, + # Add media browsers + for platform in self.hass.data[CAST_DOMAIN].values(): + children.extend( + await platform.async_get_media_browser_root_object( + self._chromecast.cast_type ) ) - # Add local media source + # Add media sources try: result = await media_source.async_browse_media( self.hass, None, content_filter=content_filter @@ -519,14 +509,15 @@ class CastDevice(MediaPlayerEntity): if media_content_id is None: return await self._async_root_payload(content_filter) - if plex.is_plex_media_id(media_content_id): - return await plex.async_browse_media( - self.hass, media_content_type, media_content_id, platform=CAST_DOMAIN - ) - if media_content_type == "plex": - return await plex.async_browse_media( - self.hass, None, None, platform=CAST_DOMAIN + for platform in self.hass.data[CAST_DOMAIN].values(): + browse_media = await platform.async_browse_media( + self.hass, + media_content_type, + media_content_id, + self._chromecast.cast_type, ) + if browse_media: + return browse_media return await media_source.async_browse_media( self.hass, media_content_id, content_filter=content_filter @@ -556,7 +547,7 @@ class CastDevice(MediaPlayerEntity): extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) metadata = extra.get("metadata") - # We do not want this to be forwarded to a group + # Handle media supported by a known cast app if media_type == CAST_DOMAIN: try: app_data = json.loads(media_id) @@ -588,23 +579,21 @@ class CastDevice(MediaPlayerEntity): ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) + return - # Handle plex - elif media_id and media_id.startswith(PLEX_URI_SCHEME): - media_id = media_id[len(PLEX_URI_SCHEME) :] - media = await self.hass.async_add_executor_job( - lookup_plex_media, self.hass, media_type, media_id + # Try the cast platforms + for platform in self.hass.data[CAST_DOMAIN].values(): + result = await platform.async_play_media( + self.hass, self.entity_id, self._chromecast, media_type, media_id ) - if media is None: + if result: return - controller = PlexController() - self._chromecast.register_handler(controller) - await self.hass.async_add_executor_job(controller.play_media, media) - else: - app_data = {"media_id": media_id, "media_type": media_type, **extra} - await self.hass.async_add_executor_job( - quick_play, self._chromecast, "default_media_receiver", app_data - ) + + # Default to play with the default media receiver + app_data = {"media_id": media_id, "media_type": media_type, **extra} + await self.hass.async_add_executor_job( + quick_play, self._chromecast, "default_media_receiver", app_data + ) def _media_status(self): """ diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py new file mode 100644 index 00000000000..c2a09ff8810 --- /dev/null +++ b/homeassistant/components/plex/cast.py @@ -0,0 +1,75 @@ +"""Google Cast support for the Plex component.""" +from __future__ import annotations + +from pychromecast import Chromecast +from pychromecast.controllers.plex import PlexController + +from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.core import HomeAssistant + +from . import async_browse_media as async_browse_plex_media, is_plex_media_id +from .const import PLEX_URI_SCHEME +from .services import lookup_plex_media + + +async def async_get_media_browser_root_object(cast_type: str) -> list[BrowseMedia]: + """Create a root object for media browsing.""" + return [ + BrowseMedia( + title="Plex", + media_class=MEDIA_CLASS_APP, + media_content_id="", + media_content_type="plex", + thumbnail="https://brands.home-assistant.io/_/plex/logo.png", + can_play=False, + can_expand=True, + ) + ] + + +async def async_browse_media( + hass: HomeAssistant, + media_content_type: str, + media_content_id: str, + cast_type: str, +) -> BrowseMedia | None: + """Browse media.""" + if is_plex_media_id(media_content_id): + return await async_browse_plex_media( + hass, media_content_type, media_content_id, platform=CAST_DOMAIN + ) + if media_content_type == "plex": + return await async_browse_plex_media(hass, None, None, platform=CAST_DOMAIN) + return None + + +def _play_media( + hass: HomeAssistant, chromecast: Chromecast, media_type: str, media_id: str +) -> None: + """Play media.""" + media_id = media_id[len(PLEX_URI_SCHEME) :] + media = lookup_plex_media(hass, media_type, media_id) + if media is None: + return + controller = PlexController() + chromecast.register_handler(controller) + controller.play_media(media) + + +async def async_play_media( + hass: HomeAssistant, + cast_entity_id: str, + chromecast: Chromecast, + media_type: str, + media_id: str, +) -> bool: + """Play media.""" + if media_id and media_id.startswith(PLEX_URI_SCHEME): + await hass.async_add_executor_job( + _play_media, hass, chromecast, media_type, media_id + ) + return True + + return False diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 2d0114c0110..3b96f378906 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -26,12 +26,6 @@ def mz_mock(): return MagicMock(spec_set=pychromecast.controllers.multizone.MultizoneManager) -@pytest.fixture() -def plex_mock(): - """Mock pychromecast PlexController.""" - return MagicMock(spec_set=pychromecast.controllers.plex.PlexController) - - @pytest.fixture() def quick_play_mock(): """Mock pychromecast quick_play.""" @@ -51,7 +45,6 @@ def cast_mock( castbrowser_mock, get_chromecast_mock, get_multizone_status_mock, - plex_mock, ): """Mock pychromecast.""" ignore_cec_orig = list(pychromecast.IGNORE_CEC) @@ -65,9 +58,6 @@ def cast_mock( ), patch( "homeassistant.components.cast.media_player.MultizoneManager", return_value=mz_mock, - ), patch( - "homeassistant.components.cast.media_player.PlexController", - return_value=plex_mock, ), patch( "homeassistant.components.cast.media_player.zeroconf.async_get_instance", AsyncMock(), diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 5e14b5e7bd6..584f40016e0 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from uuid import UUID import attr @@ -14,7 +14,10 @@ import pytest from homeassistant.components import media_player, tts from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo +from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -38,7 +41,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component, mock_platform from tests.components.media_player import common # pylint: disable=invalid-name @@ -844,54 +847,6 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): ) -async def test_entity_play_media_plex(hass: HomeAssistant, plex_mock): - """Test playing media.""" - entity_id = "media_player.speaker" - - info = get_fake_chromecast_info() - - chromecast, _ = await async_setup_media_player_cast(hass, info) - _, conn_status_cb, _ = get_status_callbacks(chromecast) - - connection_status = MagicMock() - connection_status.status = "CONNECTED" - conn_status_cb(connection_status) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.cast.media_player.lookup_plex_media", - return_value=None, - ): - await hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: entity_id, - media_player.ATTR_MEDIA_CONTENT_TYPE: "music", - media_player.ATTR_MEDIA_CONTENT_ID: 'plex://{"library_name": "Music", "artist.title": "Not an Artist"}', - }, - blocking=True, - ) - assert not plex_mock.play_media.called - - mock_plex_media = MagicMock() - with patch( - "homeassistant.components.cast.media_player.lookup_plex_media", - return_value=mock_plex_media, - ): - await hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: entity_id, - media_player.ATTR_MEDIA_CONTENT_TYPE: "music", - media_player.ATTR_MEDIA_CONTENT_ID: 'plex://{"library_name": "Music", "artist.title": "Artist"}', - }, - blocking=True, - ) - plex_mock.play_media.assert_called_once_with(mock_plex_media) - - async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): """Test playing media.""" entity_id = "media_player.speaker" @@ -1578,3 +1533,189 @@ async def test_entry_setup_list_config(hass: HomeAssistant): assert set(config_entry.data["uuid"]) == {"bla", "blu"} assert set(config_entry.data["ignore_cec"]) == {"cast1", "cast2", "cast3"} assert set(pychromecast.IGNORE_CEC) == {"cast1", "cast2", "cast3"} + + +async def test_invalid_cast_platform(hass: HomeAssistant, caplog): + """Test we can play media through a cast platform.""" + cast_platform_mock = Mock() + del cast_platform_mock.async_get_media_browser_root_object + del cast_platform_mock.async_browse_media + del cast_platform_mock.async_play_media + mock_platform(hass, "test.cast", cast_platform_mock) + + await async_setup_component(hass, "test", {"test": {}}) + await hass.async_block_till_done() + + info = get_fake_chromecast_info() + await async_setup_media_player_cast(hass, info) + + assert "Invalid cast platform Date: Mon, 31 Jan 2022 22:37:43 +1100 Subject: [PATCH 086/298] Tuya fan percentage fix (#65225) --- homeassistant/components/tuya/fan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 42849d4498d..e2e98e3fd5c 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -137,7 +137,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): [ { "code": self._speed.dpcode, - "value": int(self._speed.remap_value_from(percentage, 0, 100)), + "value": int(self._speed.remap_value_from(percentage, 1, 100)), } ] ) @@ -178,7 +178,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): commands.append( { "code": self._speed.dpcode, - "value": int(self._speed.remap_value_from(percentage, 0, 100)), + "value": int(self._speed.remap_value_from(percentage, 1, 100)), } ) return @@ -248,7 +248,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): if self._speed is not None: if (value := self.device.status.get(self._speed.dpcode)) is None: return None - return int(self._speed.remap_value_to(value, 0, 100)) + return int(self._speed.remap_value_to(value, 1, 100)) if self._speeds is not None: if (value := self.device.status.get(self._speeds.dpcode)) is None: From c5d68f866910cdb99577528f7e7fa09ee3fc4c49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:50:49 -0600 Subject: [PATCH 087/298] Increase august timeout and make failure to sync at startup non-fatal (#65281) --- homeassistant/components/august/__init__.py | 39 ++++++++++++++----- homeassistant/components/august/const.py | 2 +- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/test_init.py | 20 ++++++++++ 6 files changed, 54 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index d58541e708d..3fc113c6038 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -43,7 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await async_setup_august(hass, entry, august_gateway) except (RequireValidation, InvalidAuth) as err: raise ConfigEntryAuthFailed from err - except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err: + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady("Timed out connecting to august api") from err + except (ClientResponseError, CannotConnect) as err: raise ConfigEntryNotReady from err @@ -141,15 +143,34 @@ class AugustData(AugustSubscriberMixin): self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub) if self._locks_by_id: - tasks = [] - for lock_id in self._locks_by_id: - detail = self._device_detail_by_id[lock_id] - tasks.append( - self.async_status_async( - lock_id, bool(detail.bridge and detail.bridge.hyper_bridge) - ) + # Do not prevent setup as the sync can timeout + # but it is not a fatal error as the lock + # will recover automatically when it comes back online. + asyncio.create_task(self._async_initial_sync()) + + async def _async_initial_sync(self): + """Attempt to request an initial sync.""" + # We don't care if this fails because we only want to wake + # locks that are actually online anyways and they will be + # awake when they come back online + for result in await asyncio.gather( + *[ + self.async_status_async( + device_id, bool(detail.bridge and detail.bridge.hyper_bridge) + ) + for device_id, detail in self._device_detail_by_id.items() + if device_id in self._locks_by_id + ], + return_exceptions=True, + ): + if isinstance(result, Exception) and not isinstance( + result, (asyncio.TimeoutError, ClientResponseError, CannotConnect) + ): + _LOGGER.warning( + "Unexpected exception during initial sync: %s", + result, + exc_info=result, ) - await asyncio.gather(*tasks) @callback def async_pubnub_message(self, device_id, date_time, message): diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index dfe9cc0f700..ae3fcdf90e3 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -4,7 +4,7 @@ from datetime import timedelta from homeassistant.const import Platform -DEFAULT_TIMEOUT = 10 +DEFAULT_TIMEOUT = 15 CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file" CONF_LOGIN_METHOD = "login_method" diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index db537287b05..ea6769fabcf 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.19"], + "requirements": ["yalexs==1.1.20"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5628e197e2c..0585eb636e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2513,7 +2513,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.7 # homeassistant.components.august -yalexs==1.1.19 +yalexs==1.1.20 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2cae070fd4..9178470f838 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1541,7 +1541,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.7 # homeassistant.components.august -yalexs==1.1.19 +yalexs==1.1.20 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 4d73eec7f1f..320461ca6e9 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -30,6 +30,26 @@ from tests.components.august.mocks import ( ) +async def test_august_api_is_failing(hass): + """Config entry state is SETUP_RETRY when august api is failing.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + + with patch( + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", + side_effect=ClientResponseError(None, None, status=500), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + async def test_august_is_offline(hass): """Config entry state is SETUP_RETRY when august is offline.""" From 13ad1cc56cac25ef7f4f958c40811917f60aee96 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 Jan 2022 10:07:50 +0100 Subject: [PATCH 088/298] Bump pyatmo to v.6.2.4 (#65285) * Bump pyatmo to v6.2.3 Signed-off-by: cgtobi * Bump pyatmo to v6.2.4 Signed-off-by: cgtobi --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 581a954df30..2f133f1cdfa 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==6.2.2" + "pyatmo==6.2.4" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 0585eb636e2..62a9dcf34c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1395,7 +1395,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.2 +pyatmo==6.2.4 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9178470f838..5704729c684 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -872,7 +872,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.2 +pyatmo==6.2.4 # homeassistant.components.apple_tv pyatv==0.10.0 From 0885d481863e1d5eb9a6db44db138f5e52ec68c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 31 Jan 2022 11:45:48 +0100 Subject: [PATCH 089/298] Correct cast media browse filter for audio groups (#65288) --- homeassistant/components/cast/media_player.py | 5 +- tests/components/cast/test_media_player.py | 105 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 0b7bc5e5748..5f3381896da 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -498,7 +498,10 @@ class CastDevice(MediaPlayerEntity): """Implement the websocket media browsing helper.""" content_filter = None - if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_AUDIO: + if self._chromecast.cast_type in ( + pychromecast.const.CAST_TYPE_AUDIO, + pychromecast.const.CAST_TYPE_GROUP, + ): def audio_content_filter(item): """Filter non audio content.""" diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 584f40016e0..bd22a558314 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -759,6 +759,111 @@ async def test_supported_features( assert state.attributes.get("supported_features") == supported_features +async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): + """Test we can browse media.""" + await async_setup_component(hass, "media_source", {"media_source": {}}) + + info = get_fake_chromecast_info() + + chromecast, _ = await async_setup_media_player_cast(hass, info) + _, conn_status_cb, _ = get_status_callbacks(chromecast) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.speaker", + } + ) + response = await client.receive_json() + assert response["success"] + expected_child_1 = { + "title": "Epic Sax Guy 10 Hours.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": None, + } + assert expected_child_1 in response["result"]["children"] + + expected_child_2 = { + "title": "test.mp3", + "media_class": "music", + "media_content_type": "audio/mpeg", + "media_content_id": "media-source://media_source/local/test.mp3", + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": None, + } + assert expected_child_2 in response["result"]["children"] + + +@pytest.mark.parametrize( + "cast_type", + [pychromecast.const.CAST_TYPE_AUDIO, pychromecast.const.CAST_TYPE_GROUP], +) +async def test_entity_browse_media_audio_only( + hass: HomeAssistant, hass_ws_client, cast_type +): + """Test we can browse media.""" + await async_setup_component(hass, "media_source", {"media_source": {}}) + + info = get_fake_chromecast_info() + + chromecast, _ = await async_setup_media_player_cast(hass, info) + chromecast.cast_type = cast_type + _, conn_status_cb, _ = get_status_callbacks(chromecast) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.speaker", + } + ) + response = await client.receive_json() + assert response["success"] + expected_child_1 = { + "title": "Epic Sax Guy 10 Hours.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": None, + } + assert expected_child_1 not in response["result"]["children"] + + expected_child_2 = { + "title": "test.mp3", + "media_class": "music", + "media_content_type": "audio/mpeg", + "media_content_id": "media-source://media_source/local/test.mp3", + "can_play": True, + "can_expand": False, + "children_media_class": None, + "thumbnail": None, + } + assert expected_child_2 in response["result"]["children"] + + async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): """Test playing media.""" entity_id = "media_player.speaker" From c1019394ed3d945f25c6b1c05cb00ecce70a736f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Jan 2022 13:00:10 +0100 Subject: [PATCH 090/298] Bump pynetgear to 0.9.1 (#65290) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index e8af27cb3b6..2a81bc0b6a9 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.9.0"], + "requirements": ["pynetgear==0.9.1"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 62a9dcf34c9..301786a02ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.0 +pynetgear==0.9.1 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5704729c684..605b899d67c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1068,7 +1068,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.0 +pynetgear==0.9.1 # homeassistant.components.nina pynina==0.1.4 From 92943190483a0a556ba4daf18be9a2f0c524fbf0 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 31 Jan 2022 04:08:43 -0800 Subject: [PATCH 091/298] Bump pyoverkiz to 1.3.2 (#65293) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 192b55bec57..a7fe3d5d2fe 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": [ - "pyoverkiz==1.3.1" + "pyoverkiz==1.3.2" ], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 301786a02ab..5005932b5c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1746,7 +1746,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.1 +pyoverkiz==1.3.2 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 605b899d67c..41a42584561 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.1 +pyoverkiz==1.3.2 # homeassistant.components.openweathermap pyowm==3.2.0 From fd7f66fbdc8019756ea0397aefe6555669fb1acd Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 31 Jan 2022 12:49:18 +0100 Subject: [PATCH 092/298] Fix HomeWizard unclosed clientsession error when closing Home Assistant (#65296) --- homeassistant/components/homewizard/coordinator.py | 5 ++++- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index a2612d07464..2cce88cbe36 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -8,6 +8,7 @@ import aiohwenergy import async_timeout from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry @@ -28,7 +29,9 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] """Initialize Update Coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - self.api = aiohwenergy.HomeWizardEnergy(host) + + session = async_get_clientsession(hass) + self.api = aiohwenergy.HomeWizardEnergy(host, clientsession=session) async def _async_update_data(self) -> DeviceResponseEntry: """Fetch all device and sensor data from api.""" diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 641bfca520e..0fe2174da83 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -5,7 +5,7 @@ "codeowners": ["@DCSBL"], "dependencies": [], "requirements": [ - "aiohwenergy==0.7.0" + "aiohwenergy==0.8.0" ], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 5005932b5c5..48788881cb3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ aiohttp_cors==0.7.0 aiohue==3.0.11 # homeassistant.components.homewizard -aiohwenergy==0.7.0 +aiohwenergy==0.8.0 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41a42584561..21966b1721e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -144,7 +144,7 @@ aiohttp_cors==0.7.0 aiohue==3.0.11 # homeassistant.components.homewizard -aiohwenergy==0.7.0 +aiohwenergy==0.8.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 4f8e19ed4aca641c63ce584ddac6de0dc9449b75 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Tue, 1 Feb 2022 01:08:58 +0100 Subject: [PATCH 093/298] Add HomeWizard diagnostics (#65297) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + .../components/homewizard/diagnostics.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 homeassistant/components/homewizard/diagnostics.py diff --git a/.coveragerc b/.coveragerc index c1b2811b63c..17a10519049 100644 --- a/.coveragerc +++ b/.coveragerc @@ -463,6 +463,7 @@ omit = homeassistant/components/homematic/* homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py + homeassistant/components/homewizard/diagnostics.py homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py new file mode 100644 index 00000000000..3dd55933291 --- /dev/null +++ b/homeassistant/components/homewizard/diagnostics.py @@ -0,0 +1,34 @@ +"""Diagnostics support for P1 Monitor.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import HWEnergyDeviceUpdateCoordinator + +TO_REDACT = {CONF_IP_ADDRESS, "serial", "wifi_ssid"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + meter_data = { + "device": coordinator.api.device.todict(), + "data": coordinator.api.data.todict(), + "state": coordinator.api.state.todict() + if coordinator.api.state is not None + else None, + } + + return { + "entry": async_redact_data(entry.data, TO_REDACT), + "data": async_redact_data(meter_data, TO_REDACT), + } From 0519b2950107dbf9bc0b95ee882646256b8dfa3e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Jan 2022 16:31:02 +0100 Subject: [PATCH 094/298] Update adguard to 0.5.1 (#65305) --- homeassistant/components/adguard/config_flow.py | 4 ++-- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index aadbed49980..9afdc4e02b8 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -80,8 +80,8 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): adguard = AdGuardHome( user_input[CONF_HOST], port=user_input[CONF_PORT], - username=username, # type:ignore[arg-type] - password=password, # type:ignore[arg-type] + username=username, + password=password, tls=user_input[CONF_SSL], verify_ssl=user_input[CONF_VERIFY_SSL], session=session, diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index bd311dd3d35..9dc72ad76d9 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -3,7 +3,7 @@ "name": "AdGuard Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": ["adguardhome==0.5.0"], + "requirements": ["adguardhome==0.5.1"], "codeowners": ["@frenck"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 48788881cb3..c6412d40154 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -111,7 +111,7 @@ adb-shell[async]==0.4.0 adext==0.4.2 # homeassistant.components.adguard -adguardhome==0.5.0 +adguardhome==0.5.1 # homeassistant.components.advantage_air advantage_air==0.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21966b1721e..fcd3336e1c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -67,7 +67,7 @@ adb-shell[async]==0.4.0 adext==0.4.2 # homeassistant.components.adguard -adguardhome==0.5.0 +adguardhome==0.5.1 # homeassistant.components.advantage_air advantage_air==0.2.5 From 5dc92bb2ce5a6a316df4bba463acfec3038bc0d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Jan 2022 17:08:21 +0100 Subject: [PATCH 095/298] Update wled to 0.13.0 (#65312) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index d99f07a78ae..eb99b8519a9 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.12.0"], + "requirements": ["wled==0.13.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index c6412d40154..f96f72679e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2481,7 +2481,7 @@ wirelesstagpy==0.8.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.12.0 +wled==0.13.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcd3336e1c1..2f429ad55c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1518,7 +1518,7 @@ wiffi==1.1.0 withings-api==2.3.2 # homeassistant.components.wled -wled==0.12.0 +wled==0.13.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 711739548919c175830f8afd29cd828c42882c98 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Jan 2022 18:15:13 +0100 Subject: [PATCH 096/298] Fix missing expiration data in Whois information (#65313) --- homeassistant/components/whois/sensor.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 6df920f385c..3d0b25640b3 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -233,17 +233,20 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): if self.coordinator.data is None: return None - attrs = { - ATTR_EXPIRES: self.coordinator.data.expiration_date.isoformat(), - } + attrs = {} + if expiration_date := self.coordinator.data.expiration_date: + attrs[ATTR_EXPIRES] = expiration_date.isoformat() - if self.coordinator.data.name_servers: - attrs[ATTR_NAME_SERVERS] = " ".join(self.coordinator.data.name_servers) + if name_servers := self.coordinator.data.name_servers: + attrs[ATTR_NAME_SERVERS] = " ".join(name_servers) - if self.coordinator.data.last_updated: - attrs[ATTR_UPDATED] = self.coordinator.data.last_updated.isoformat() + if last_updated := self.coordinator.data.last_updated: + attrs[ATTR_UPDATED] = last_updated.isoformat() - if self.coordinator.data.registrar: - attrs[ATTR_REGISTRAR] = self.coordinator.data.registrar + if registrar := self.coordinator.data.registrar: + attrs[ATTR_REGISTRAR] = registrar + + if not attrs: + return None return attrs From 961cf15e6e60ab30cb5d5d2b44c6917d0da7e259 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jan 2022 10:21:47 -0600 Subject: [PATCH 097/298] Improve reliability of august setup with recent api changes (#65314) --- homeassistant/components/august/__init__.py | 28 ++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 3fc113c6038..5d51017bfd6 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -206,12 +206,28 @@ class AugustData(AugustSubscriberMixin): await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) async def _async_refresh_device_detail_by_ids(self, device_ids_list): - await asyncio.gather( - *( - self._async_refresh_device_detail_by_id(device_id) - for device_id in device_ids_list - ) - ) + """Refresh each device in sequence. + + This used to be a gather but it was less reliable with august's + recent api changes. + + The august api has been timing out for some devices so + we want the ones that it isn't timing out for to keep working. + """ + for device_id in device_ids_list: + try: + await self._async_refresh_device_detail_by_id(device_id) + except asyncio.TimeoutError: + _LOGGER.warning( + "Timed out calling august api during refresh of device: %s", + device_id, + ) + except (ClientResponseError, CannotConnect) as err: + _LOGGER.warning( + "Error from august api during refresh of device: %s", + device_id, + exc_info=err, + ) async def _async_refresh_device_detail_by_id(self, device_id): if device_id in self._locks_by_id: From 00b2c85e987a7db6af4ba4b5a1cba494c73dc6d8 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 31 Jan 2022 09:09:17 -0800 Subject: [PATCH 098/298] Bump androidtv to 0.0.61 (#65315) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index f50876e6629..5a515dd4816 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.4.0", - "androidtv[async]==0.0.60", + "androidtv[async]==0.0.61", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion", "@ollo69"], diff --git a/requirements_all.txt b/requirements_all.txt index f96f72679e9..0ea2173353a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -311,7 +311,7 @@ ambiclimate==0.2.1 amcrest==1.9.3 # homeassistant.components.androidtv -androidtv[async]==0.0.60 +androidtv[async]==0.0.61 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f429ad55c9..85b17076331 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -237,7 +237,7 @@ amberelectric==1.0.3 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.60 +androidtv[async]==0.0.61 # homeassistant.components.apns apns2==0.3.0 From ea511357b684f8d3e70eeb9c133c986c10cae192 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Jan 2022 18:16:33 +0100 Subject: [PATCH 099/298] Add diagnostics support to WLED (#65317) --- homeassistant/components/wled/diagnostics.py | 48 +++++ tests/components/wled/test_diagnostics.py | 210 +++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 homeassistant/components/wled/diagnostics.py create mode 100644 tests/components/wled/test_diagnostics.py diff --git a/homeassistant/components/wled/diagnostics.py b/homeassistant/components/wled/diagnostics.py new file mode 100644 index 00000000000..c2820a7a13a --- /dev/null +++ b/homeassistant/components/wled/diagnostics.py @@ -0,0 +1,48 @@ +"""Diagnostics support for WLED.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + data = { + "info": async_redact_data(coordinator.data.info.__dict__, "wifi"), + "state": coordinator.data.state.__dict__, + "effects": { + effect.effect_id: effect.name for effect in coordinator.data.effects + }, + "palettes": { + palette.palette_id: palette.name for palette in coordinator.data.palettes + }, + "playlists": { + playlist.playlist_id: { + "name": playlist.name, + "repeat": playlist.repeat, + "shuffle": playlist.shuffle, + "end": playlist.end.preset_id if playlist.end else None, + } + for playlist in coordinator.data.playlists + }, + "presets": { + preset.preset_id: { + "name": preset.name, + "quick_label": preset.quick_label, + "on": preset.on, + "transition": preset.transition, + } + for preset in coordinator.data.presets + }, + } + return data diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py new file mode 100644 index 00000000000..d8782848c92 --- /dev/null +++ b/tests/components/wled/test_diagnostics.py @@ -0,0 +1,210 @@ +"""Tests for the diagnostics data provided by the WLED integration.""" +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "info": { + "architecture": "esp8266", + "arduino_core_version": "2.4.2", + "brand": "WLED", + "build_type": "bin", + "effect_count": 81, + "filesystem": None, + "free_heap": 14600, + "leds": { + "__type": "", + "repr": "Leds(cct=False, count=30, fps=None, max_power=850, max_segments=10, power=470, rgbw=False, wv=True)", + }, + "live_ip": "Unknown", + "live_mode": "Unknown", + "live": False, + "mac_address": "aabbccddeeff", + "name": "WLED RGB Light", + "pallet_count": 50, + "product": "DIY light", + "udp_port": 21324, + "uptime": 32, + "version_id": 1909122, + "version": "0.8.5", + "version_latest_beta": "0.13.0b1", + "version_latest_stable": "0.12.0", + "websocket": None, + "wifi": "**REDACTED**", + }, + "state": { + "brightness": 127, + "nightlight": { + "__type": "", + "repr": "Nightlight(duration=60, fade=True, on=False, mode=, target_brightness=0)", + }, + "on": True, + "playlist": -1, + "preset": -1, + "segments": [ + { + "__type": "", + "repr": "Segment(brightness=127, clones=-1, color_primary=(255, 159, 0), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=0, name='Solid'), intensity=128, length=20, on=True, palette=Palette(name='Default', palette_id=0), reverse=False, segment_id=0, selected=True, speed=32, start=0, stop=19)", + }, + { + "__type": "", + "repr": "Segment(brightness=127, clones=-1, color_primary=(0, 255, 123), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=1, name='Blink'), intensity=64, length=10, on=True, palette=Palette(name='Random Cycle', palette_id=1), reverse=True, segment_id=1, selected=True, speed=16, start=20, stop=30)", + }, + ], + "sync": { + "__type": "", + "repr": "Sync(receive=True, send=False)", + }, + "transition": 7, + "lor": 0, + }, + "effects": { + "27": "Android", + "68": "BPM", + "1": "Blink", + "26": "Blink Rainbow", + "2": "Breathe", + "13": "Chase", + "28": "Chase", + "31": "Chase Flash", + "32": "Chase Flash Rnd", + "14": "Chase Rainbow", + "30": "Chase Rainbow", + "29": "Chase Random", + "52": "Circus", + "34": "Colorful", + "8": "Colorloop", + "74": "Colortwinkle", + "67": "Colorwaves", + "21": "Dark Sparkle", + "18": "Dissolve", + "19": "Dissolve Rnd", + "11": "Dual Scan", + "60": "Dual Scanner", + "7": "Dynamic", + "12": "Fade", + "69": "Fill Noise", + "66": "Fire 2012", + "45": "Fire Flicker", + "42": "Fireworks", + "46": "Gradient", + "53": "Halloween", + "58": "ICU", + "49": "In In", + "48": "In Out", + "64": "Juggle", + "75": "Lake", + "41": "Lighthouse", + "57": "Lightning", + "47": "Loading", + "25": "Mega Strobe", + "44": "Merry Christmas", + "76": "Meteor", + "59": "Multi Comet", + "70": "Noise 1", + "71": "Noise 2", + "72": "Noise 3", + "73": "Noise 4", + "62": "Oscillate", + "51": "Out In", + "50": "Out Out", + "65": "Palette", + "63": "Pride 2015", + "78": "Railway", + "43": "Rain", + "9": "Rainbow", + "33": "Rainbow Runner", + "5": "Random Colors", + "38": "Red & Blue", + "79": "Ripple", + "15": "Running", + "37": "Running 2", + "16": "Saw", + "10": "Scan", + "40": "Scanner", + "77": "Smooth Meteor", + "0": "Solid", + "20": "Sparkle", + "22": "Sparkle+", + "39": "Stream", + "61": "Stream 2", + "23": "Strobe", + "24": "Strobe Rainbow", + "6": "Sweep", + "36": "Sweep Random", + "35": "Traffic Light", + "54": "Tri Chase", + "56": "Tri Fade", + "55": "Tri Wipe", + "17": "Twinkle", + "80": "Twinklefox", + "3": "Wipe", + "4": "Wipe Random", + }, + "palettes": { + "18": "Analogous", + "46": "April Night", + "39": "Autumn", + "3": "Based on Primary", + "5": "Based on Set", + "26": "Beach", + "22": "Beech", + "15": "Breeze", + "48": "C9", + "7": "Cloud", + "37": "Cyane", + "0": "Default", + "24": "Departure", + "30": "Drywet", + "35": "Fire", + "10": "Forest", + "32": "Grintage", + "28": "Hult", + "29": "Hult 64", + "36": "Icefire", + "31": "Jul", + "25": "Landscape", + "8": "Lava", + "38": "Light Pink", + "40": "Magenta", + "41": "Magred", + "9": "Ocean", + "44": "Orange & Teal", + "47": "Orangery", + "6": "Party", + "20": "Pastel", + "2": "Primary Color", + "11": "Rainbow", + "12": "Rainbow Bands", + "1": "Random Cycle", + "16": "Red & Blue", + "33": "Rewhi", + "14": "Rivendell", + "49": "Sakura", + "4": "Set Colors", + "27": "Sherbet", + "19": "Splash", + "13": "Sunset", + "21": "Sunset 2", + "34": "Tertiary", + "45": "Tiamat", + "23": "Vintage", + "43": "Yelblu", + "17": "Yellowout", + "42": "Yelmag", + }, + "playlists": {}, + "presets": {}, + } From 87b20c6abe7b2510bb51244689cf655075adf983 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Jan 2022 18:17:35 +0100 Subject: [PATCH 100/298] Update tailscale to 0.2.0 (#65318) --- homeassistant/components/tailscale/binary_sensor.py | 6 ++---- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index fff4cfbf908..2f97d307b15 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -111,8 +111,6 @@ class TailscaleBinarySensorEntity(TailscaleEntity, BinarySensorEntity): entity_description: TailscaleBinarySensorEntityDescription @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return the state of the sensor.""" - return bool( - self.entity_description.is_on_fn(self.coordinator.data[self.device_id]) - ) + return self.entity_description.is_on_fn(self.coordinator.data[self.device_id]) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index ac7cbe84459..3249705ce7c 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.6"], + "requirements": ["tailscale==0.2.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 0ea2173353a..e5f4b8aaa08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ synology-srm==0.2.0 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.6 +tailscale==0.2.0 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85b17076331..7c5f17547ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1420,7 +1420,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.6 +tailscale==0.2.0 # homeassistant.components.tellduslive tellduslive==0.10.11 From 74632d26faa04a74f8c409905f0eb70c644d2913 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Jan 2022 18:42:49 +0100 Subject: [PATCH 101/298] Ensure PVOutput connection error is logged (#65319) --- homeassistant/components/pvoutput/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index aa197061466..a1933ff9315 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_SYSTEM_ID, DOMAIN +from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) -> None: @@ -50,6 +50,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): except PVOutputAuthenticationError: errors["base"] = "invalid_auth" except PVOutputError: + LOGGER.exception("Cannot connect to PVOutput") errors["base"] = "cannot_connect" else: await self.async_set_unique_id(str(user_input[CONF_SYSTEM_ID])) From 0a000babc98d1d49f862e225f64047546e5f5100 Mon Sep 17 00:00:00 2001 From: Pascal Winters Date: Mon, 31 Jan 2022 19:23:07 +0100 Subject: [PATCH 102/298] Bump pyps4-2ndscreen to 1.3.1 (#65320) --- homeassistant/components/ps4/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 609b7497744..799615a8cba 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,7 +3,7 @@ "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": ["pyps4-2ndscreen==1.2.0"], + "requirements": ["pyps4-2ndscreen==1.3.1"], "codeowners": ["@ktnrg45"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index e5f4b8aaa08..bb44e203352 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1776,7 +1776,7 @@ pyprof2calltree==1.4.5 pyprosegur==0.0.5 # homeassistant.components.ps4 -pyps4-2ndscreen==1.2.0 +pyps4-2ndscreen==1.3.1 # homeassistant.components.qvr_pro pyqvrpro==0.52 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c5f17547ad..adf7c1dd3c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1130,7 +1130,7 @@ pyprof2calltree==1.4.5 pyprosegur==0.0.5 # homeassistant.components.ps4 -pyps4-2ndscreen==1.2.0 +pyps4-2ndscreen==1.3.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 From 1fbd624a246b328e4dbf477bd3a7e1e2f48929d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Jan 2022 15:01:46 -0800 Subject: [PATCH 103/298] Alexa to handle brightness and catch exceptions (#65322) --- homeassistant/components/alexa/errors.py | 26 ++++++++++++++++++++ homeassistant/components/alexa/handlers.py | 14 +++-------- homeassistant/components/alexa/smart_home.py | 12 ++++++++- tests/components/alexa/__init__.py | 4 ++- tests/components/alexa/test_capabilities.py | 20 +++++---------- 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index f4c50a24267..0ce00f1fe48 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -1,6 +1,8 @@ """Alexa related errors.""" from __future__ import annotations +from typing import Literal + from homeassistant.exceptions import HomeAssistantError from .const import API_TEMP_UNITS @@ -58,6 +60,30 @@ class AlexaInvalidValueError(AlexaError): error_type = "INVALID_VALUE" +class AlexaInteralError(AlexaError): + """Class to represent internal errors.""" + + namespace = "Alexa" + error_type = "INTERNAL_ERROR" + + +class AlexaNotSupportedInCurrentMode(AlexaError): + """The device is not in the correct mode to support this command.""" + + namespace = "Alexa" + error_type = "NOT_SUPPORTED_IN_CURRENT_MODE" + + def __init__( + self, + endpoint_id: str, + current_mode: Literal["COLOR", "ASLEEP", "NOT_PROVISIONED", "OTHER"], + ) -> None: + """Initialize invalid endpoint error.""" + msg = f"Not supported while in {current_mode} mode" + AlexaError.__init__(self, msg, {"currentDeviceMode": current_mode}) + self.endpoint_id = endpoint_id + + class AlexaUnsupportedThermostatModeError(AlexaError): """Class to represent UnsupportedThermostatMode errors.""" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c0b0782f62e..f3f669de3b3 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -212,20 +212,14 @@ async def async_api_adjust_brightness(hass, config, directive, context): entity = directive.entity brightness_delta = int(directive.payload["brightnessDelta"]) - # read current state - try: - current = math.floor( - int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100 - ) - except ZeroDivisionError: - current = 0 - # set brightness - brightness = max(0, brightness_delta + current) await hass.services.async_call( entity.domain, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness}, + { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_BRIGHTNESS_STEP_PCT: brightness_delta, + }, blocking=False, context=context, ) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 7d144619bc9..24229507877 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -48,8 +48,18 @@ async def async_handle_message(hass, config, request, context=None, enabled=True response = directive.error() except AlexaError as err: response = directive.error( - error_type=err.error_type, error_message=err.error_message + error_type=err.error_type, + error_message=err.error_message, + payload=err.payload, ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Uncaught exception processing Alexa %s/%s request (%s)", + directive.namespace, + directive.name, + directive.entity_id or "-", + ) + response = directive.error(error_message="Unknown error") request_info = {"namespace": directive.namespace, "name": directive.name} diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 1d8289b5ec0..053100d2e00 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -194,7 +194,7 @@ async def assert_scene_controller_works( assert re.search(pattern, response["event"]["payload"]["timestamp"]) -async def reported_properties(hass, endpoint): +async def reported_properties(hass, endpoint, return_full_response=False): """Use ReportState to get properties and return them. The result is a ReportedProperties instance, which has methods to make @@ -203,6 +203,8 @@ async def reported_properties(hass, endpoint): request = get_new_request("Alexa", "ReportState", endpoint) msg = await smart_home.async_handle_message(hass, get_default_config(), request) await hass.async_block_till_done() + if return_full_response: + return msg return ReportedProperties(msg["context"]["properties"]) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 566917d7c39..8a9a40e3217 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from homeassistant.components.alexa import smart_home -from homeassistant.components.alexa.errors import UnsupportedProperty from homeassistant.components.climate import const as climate from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING from homeassistant.components.media_player.const import ( @@ -39,8 +38,8 @@ from . import ( from tests.common import async_mock_service -@pytest.mark.parametrize("result,adjust", [(25, "-5"), (35, "5"), (0, "-80")]) -async def test_api_adjust_brightness(hass, result, adjust): +@pytest.mark.parametrize("adjust", ["-5", "5", "-80"]) +async def test_api_adjust_brightness(hass, adjust): """Test api adjust brightness process.""" request = get_new_request( "Alexa.BrightnessController", "AdjustBrightness", "light#test" @@ -64,7 +63,7 @@ async def test_api_adjust_brightness(hass, result, adjust): assert len(call_light) == 1 assert call_light[0].data["entity_id"] == "light.test" - assert call_light[0].data["brightness_pct"] == result + assert call_light[0].data["brightness_step_pct"] == int(adjust) assert msg["header"]["name"] == "Response" @@ -677,16 +676,9 @@ async def test_report_climate_state(hass): ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ) - with pytest.raises(UnsupportedProperty): - properties = await reported_properties(hass, "climate.unsupported") - properties.assert_not_has_property( - "Alexa.ThermostatController", "thermostatMode" - ) - properties.assert_equal( - "Alexa.TemperatureSensor", - "temperature", - {"value": 34.0, "scale": "CELSIUS"}, - ) + msg = await reported_properties(hass, "climate.unsupported", True) + assert msg["event"]["header"]["name"] == "ErrorResponse" + assert msg["event"]["payload"]["type"] == "INTERNAL_ERROR" async def test_temperature_sensor_sensor(hass): From 1facd0edd4aa71795dff9a21675c162a8001b722 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 29 Jan 2022 06:06:19 +0100 Subject: [PATCH 104/298] Fritz tests cleanup (#65054) --- tests/components/fritz/__init__.py | 126 --------------------- tests/components/fritz/conftest.py | 116 +++++++++++++++++++ tests/components/fritz/const.py | 48 ++++++++ tests/components/fritz/test_config_flow.py | 47 ++------ 4 files changed, 173 insertions(+), 164 deletions(-) create mode 100644 tests/components/fritz/conftest.py create mode 100644 tests/components/fritz/const.py diff --git a/tests/components/fritz/__init__.py b/tests/components/fritz/__init__.py index a1fd1ce42fb..1462ec77b8f 100644 --- a/tests/components/fritz/__init__.py +++ b/tests/components/fritz/__init__.py @@ -1,127 +1 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest import mock - -from homeassistant.components.fritz.const import DOMAIN -from homeassistant.const import ( - CONF_DEVICES, - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) - -MOCK_CONFIG = { - DOMAIN: { - CONF_DEVICES: [ - { - CONF_HOST: "fake_host", - CONF_PORT: "1234", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] - } -} - - -class FritzConnectionMock: # pylint: disable=too-few-public-methods - """FritzConnection mocking.""" - - FRITZBOX_DATA = { - ("WANIPConn:1", "GetStatusInfo"): { - "NewConnectionStatus": "Connected", - "NewUptime": 35307, - }, - ("WANIPConnection:1", "GetStatusInfo"): {}, - ("WANCommonIFC:1", "GetCommonLinkProperties"): { - "NewLayer1DownstreamMaxBitRate": 10087000, - "NewLayer1UpstreamMaxBitRate": 2105000, - "NewPhysicalLinkStatus": "Up", - }, - ("WANCommonIFC:1", "GetAddonInfos"): { - "NewByteSendRate": 3438, - "NewByteReceiveRate": 67649, - "NewTotalBytesSent": 1712232562, - "NewTotalBytesReceived": 5221019883, - }, - ("LANEthernetInterfaceConfig:1", "GetStatistics"): { - "NewBytesSent": 23004321, - "NewBytesReceived": 12045, - }, - ("DeviceInfo:1", "GetInfo"): { - "NewSerialNumber": "abcdefgh", - "NewName": "TheName", - "NewModelName": "FRITZ!Box 7490", - }, - } - - FRITZBOX_DATA_INDEXED = { - ("X_AVM-DE_Homeauto:1", "GetGenericDeviceInfos"): [ - { - "NewSwitchIsValid": "VALID", - "NewMultimeterIsValid": "VALID", - "NewTemperatureIsValid": "VALID", - "NewDeviceId": 16, - "NewAIN": "08761 0114116", - "NewDeviceName": "FRITZ!DECT 200 #1", - "NewTemperatureOffset": "0", - "NewSwitchLock": "0", - "NewProductName": "FRITZ!DECT 200", - "NewPresent": "CONNECTED", - "NewMultimeterPower": 1673, - "NewHkrComfortTemperature": "0", - "NewSwitchMode": "AUTO", - "NewManufacturer": "AVM", - "NewMultimeterIsEnabled": "ENABLED", - "NewHkrIsTemperature": "0", - "NewFunctionBitMask": 2944, - "NewTemperatureIsEnabled": "ENABLED", - "NewSwitchState": "ON", - "NewSwitchIsEnabled": "ENABLED", - "NewFirmwareVersion": "03.87", - "NewHkrSetVentilStatus": "CLOSED", - "NewMultimeterEnergy": 5182, - "NewHkrComfortVentilStatus": "CLOSED", - "NewHkrReduceTemperature": "0", - "NewHkrReduceVentilStatus": "CLOSED", - "NewHkrIsEnabled": "DISABLED", - "NewHkrSetTemperature": "0", - "NewTemperatureCelsius": "225", - "NewHkrIsValid": "INVALID", - }, - {}, - ], - ("Hosts1", "GetGenericHostEntry"): [ - { - "NewSerialNumber": 1234, - "NewName": "TheName", - "NewModelName": "FRITZ!Box 7490", - }, - {}, - ], - } - - MODELNAME = "FRITZ!Box 7490" - - def __init__(self): - """Inint Mocking class.""" - type(self).modelname = mock.PropertyMock(return_value=self.MODELNAME) - self.call_action = mock.Mock(side_effect=self._side_effect_call_action) - type(self).action_names = mock.PropertyMock( - side_effect=self._side_effect_action_names - ) - services = { - srv: None - for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) - } - type(self).services = mock.PropertyMock(side_effect=[services]) - - def _side_effect_call_action(self, service, action, **kwargs): - if kwargs: - index = next(iter(kwargs.values())) - return self.FRITZBOX_DATA_INDEXED[(service, action)][index] - - return self.FRITZBOX_DATA[(service, action)] - - def _side_effect_action_names(self): - return list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py new file mode 100644 index 00000000000..6f99ab483e6 --- /dev/null +++ b/tests/components/fritz/conftest.py @@ -0,0 +1,116 @@ +"""Common stuff for AVM Fritz!Box tests.""" +from unittest import mock +from unittest.mock import patch + +import pytest + + +@pytest.fixture() +def fc_class_mock(): + """Fixture that sets up a mocked FritzConnection class.""" + with patch("fritzconnection.FritzConnection", autospec=True) as result: + result.return_value = FritzConnectionMock() + yield result + + +class FritzConnectionMock: # pylint: disable=too-few-public-methods + """FritzConnection mocking.""" + + FRITZBOX_DATA = { + ("WANIPConn:1", "GetStatusInfo"): { + "NewConnectionStatus": "Connected", + "NewUptime": 35307, + }, + ("WANIPConnection:1", "GetStatusInfo"): {}, + ("WANCommonIFC:1", "GetCommonLinkProperties"): { + "NewLayer1DownstreamMaxBitRate": 10087000, + "NewLayer1UpstreamMaxBitRate": 2105000, + "NewPhysicalLinkStatus": "Up", + }, + ("WANCommonIFC:1", "GetAddonInfos"): { + "NewByteSendRate": 3438, + "NewByteReceiveRate": 67649, + "NewTotalBytesSent": 1712232562, + "NewTotalBytesReceived": 5221019883, + }, + ("LANEthernetInterfaceConfig:1", "GetStatistics"): { + "NewBytesSent": 23004321, + "NewBytesReceived": 12045, + }, + ("DeviceInfo:1", "GetInfo"): { + "NewSerialNumber": "abcdefgh", + "NewName": "TheName", + "NewModelName": "FRITZ!Box 7490", + }, + } + + FRITZBOX_DATA_INDEXED = { + ("X_AVM-DE_Homeauto:1", "GetGenericDeviceInfos"): [ + { + "NewSwitchIsValid": "VALID", + "NewMultimeterIsValid": "VALID", + "NewTemperatureIsValid": "VALID", + "NewDeviceId": 16, + "NewAIN": "08761 0114116", + "NewDeviceName": "FRITZ!DECT 200 #1", + "NewTemperatureOffset": "0", + "NewSwitchLock": "0", + "NewProductName": "FRITZ!DECT 200", + "NewPresent": "CONNECTED", + "NewMultimeterPower": 1673, + "NewHkrComfortTemperature": "0", + "NewSwitchMode": "AUTO", + "NewManufacturer": "AVM", + "NewMultimeterIsEnabled": "ENABLED", + "NewHkrIsTemperature": "0", + "NewFunctionBitMask": 2944, + "NewTemperatureIsEnabled": "ENABLED", + "NewSwitchState": "ON", + "NewSwitchIsEnabled": "ENABLED", + "NewFirmwareVersion": "03.87", + "NewHkrSetVentilStatus": "CLOSED", + "NewMultimeterEnergy": 5182, + "NewHkrComfortVentilStatus": "CLOSED", + "NewHkrReduceTemperature": "0", + "NewHkrReduceVentilStatus": "CLOSED", + "NewHkrIsEnabled": "DISABLED", + "NewHkrSetTemperature": "0", + "NewTemperatureCelsius": "225", + "NewHkrIsValid": "INVALID", + }, + {}, + ], + ("Hosts1", "GetGenericHostEntry"): [ + { + "NewSerialNumber": 1234, + "NewName": "TheName", + "NewModelName": "FRITZ!Box 7490", + }, + {}, + ], + } + + MODELNAME = "FRITZ!Box 7490" + + def __init__(self): + """Inint Mocking class.""" + type(self).modelname = mock.PropertyMock(return_value=self.MODELNAME) + self.call_action = mock.Mock(side_effect=self._side_effect_call_action) + type(self).action_names = mock.PropertyMock( + side_effect=self._side_effect_action_names + ) + services = { + srv: None + for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) + } + type(self).services = mock.PropertyMock(side_effect=[services]) + + def _side_effect_call_action(self, service, action, **kwargs): + if kwargs: + index = next(iter(kwargs.values())) + return self.FRITZBOX_DATA_INDEXED[(service, action)][index] + + return self.FRITZBOX_DATA[(service, action)] + + def _side_effect_action_names(self): + return list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py new file mode 100644 index 00000000000..3212794fc85 --- /dev/null +++ b/tests/components/fritz/const.py @@ -0,0 +1,48 @@ +"""Common stuff for AVM Fritz!Box tests.""" +from homeassistant.components import ssdp +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +ATTR_HOST = "host" +ATTR_NEW_SERIAL_NUMBER = "NewSerialNumber" + +MOCK_CONFIG = { + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_PORT: "1234", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] + } +} +MOCK_HOST = "fake_host" +MOCK_IP = "192.168.178.1" +MOCK_SERIAL_NUMBER = "fake_serial_number" +MOCK_FIRMWARE_INFO = [True, "1.1.1"] + +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] +MOCK_DEVICE_INFO = { + ATTR_HOST: MOCK_HOST, + ATTR_NEW_SERIAL_NUMBER: MOCK_SERIAL_NUMBER, +} +MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"https://{MOCK_IP}:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "fake_name", + ATTR_UPNP_UDN: "uuid:only-a-test", + }, +) + +MOCK_REQUEST = b'xxxxxxxxxxxxxxxxxxxxxxxx0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2FakeFritzUser\n' diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index edb03c51603..6505ee2bcaa 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -3,9 +3,7 @@ import dataclasses from unittest.mock import patch from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError -import pytest -from homeassistant.components import ssdp from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -16,9 +14,9 @@ from homeassistant.components.fritz.const import ( ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, ) -from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN +from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -26,42 +24,15 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import MOCK_CONFIG, FritzConnectionMock - -from tests.common import MockConfigEntry - -ATTR_HOST = "host" -ATTR_NEW_SERIAL_NUMBER = "NewSerialNumber" - -MOCK_HOST = "fake_host" -MOCK_IP = "192.168.178.1" -MOCK_SERIAL_NUMBER = "fake_serial_number" -MOCK_FIRMWARE_INFO = [True, "1.1.1"] - -MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] -MOCK_DEVICE_INFO = { - ATTR_HOST: MOCK_HOST, - ATTR_NEW_SERIAL_NUMBER: MOCK_SERIAL_NUMBER, -} -MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location=f"https://{MOCK_IP}:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "fake_name", - ATTR_UPNP_UDN: "uuid:only-a-test", - }, +from .const import ( + MOCK_FIRMWARE_INFO, + MOCK_IP, + MOCK_REQUEST, + MOCK_SSDP_DATA, + MOCK_USER_DATA, ) -MOCK_REQUEST = b'xxxxxxxxxxxxxxxxxxxxxxxx0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2FakeFritzUser\n' - - -@pytest.fixture() -def fc_class_mock(): - """Fixture that sets up a mocked FritzConnection class.""" - with patch("fritzconnection.FritzConnection", autospec=True) as result: - result.return_value = FritzConnectionMock() - yield result +from tests.common import MockConfigEntry async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): From 649b4ce329405e5f55a3c0cd2d2525965ac42643 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 1 Feb 2022 00:28:11 +0100 Subject: [PATCH 105/298] Improve debugging and error handling in Fritz!Tools (#65324) --- homeassistant/components/fritz/__init__.py | 10 +++----- homeassistant/components/fritz/common.py | 29 ++++++++++++++-------- homeassistant/components/fritz/const.py | 16 ++++++++++++ tests/components/fritz/conftest.py | 5 ++-- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 0db85b12077..a0e0413366b 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -1,11 +1,7 @@ """Support for AVM Fritz!Box functions.""" import logging -from fritzconnection.core.exceptions import ( - FritzConnectionException, - FritzResourceError, - FritzSecurityError, -) +from fritzconnection.core.exceptions import FritzSecurityError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -13,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .common import AvmWrapper, FritzData -from .const import DATA_FRITZ, DOMAIN, PLATFORMS +from .const import DATA_FRITZ, DOMAIN, FRITZ_EXCEPTIONS, PLATFORMS from .services import async_setup_services, async_unload_services _LOGGER = logging.getLogger(__name__) @@ -34,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await avm_wrapper.async_setup(entry.options) except FritzSecurityError as ex: raise ConfigEntryAuthFailed from ex - except (FritzConnectionException, FritzResourceError) as ex: + except FRITZ_EXCEPTIONS as ex: raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 70485ac0c5f..c9eff204bf2 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -12,10 +12,7 @@ from typing import Any, TypedDict, cast from fritzconnection import FritzConnection from fritzconnection.core.exceptions import ( FritzActionError, - FritzActionFailedError, FritzConnectionException, - FritzInternalError, - FritzLookUpError, FritzSecurityError, FritzServiceError, ) @@ -46,6 +43,7 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, + FRITZ_EXCEPTIONS, SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT, @@ -188,9 +186,26 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): _LOGGER.error("Unable to establish a connection with %s", self.host) return + _LOGGER.debug( + "detected services on %s %s", + self.host, + list(self.connection.services.keys()), + ) + self.fritz_hosts = FritzHosts(fc=self.connection) self.fritz_status = FritzStatus(fc=self.connection) info = self.connection.call_action("DeviceInfo:1", "GetInfo") + + _LOGGER.debug( + "gathered device info of %s %s", + self.host, + { + **info, + "NewDeviceLog": "***omitted***", + "NewSerialNumber": "***omitted***", + }, + ) + if not self._unique_id: self._unique_id = info["NewSerialNumber"] @@ -529,13 +544,7 @@ class AvmWrapper(FritzBoxTools): "Authorization Error: Please check the provided credentials and verify that you can log into the web interface", exc_info=True, ) - except ( - FritzActionError, - FritzActionFailedError, - FritzInternalError, - FritzServiceError, - FritzLookUpError, - ): + except FRITZ_EXCEPTIONS: _LOGGER.error( "Service/Action Error: cannot execute service %s with action %s", service_name, diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index ae8ffe83e38..59200e07c78 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -2,6 +2,14 @@ from typing import Literal +from fritzconnection.core.exceptions import ( + FritzActionError, + FritzActionFailedError, + FritzInternalError, + FritzLookUpError, + FritzServiceError, +) + from homeassistant.backports.enum import StrEnum from homeassistant.const import Platform @@ -47,3 +55,11 @@ SWITCH_TYPE_PORTFORWARD = "PortForward" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" UPTIME_DEVIATION = 5 + +FRITZ_EXCEPTIONS = ( + FritzActionError, + FritzActionFailedError, + FritzInternalError, + FritzServiceError, + FritzLookUpError, +) diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 6f99ab483e6..1dc60f4a59e 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -94,16 +94,15 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods def __init__(self): """Inint Mocking class.""" - type(self).modelname = mock.PropertyMock(return_value=self.MODELNAME) + self.modelname = self.MODELNAME self.call_action = mock.Mock(side_effect=self._side_effect_call_action) type(self).action_names = mock.PropertyMock( side_effect=self._side_effect_action_names ) - services = { + self.services = { srv: None for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) } - type(self).services = mock.PropertyMock(side_effect=[services]) def _side_effect_call_action(self, service, action, **kwargs): if kwargs: From eea9e26ef5025ffe7385dfba4e8e632cc87f60c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jan 2022 17:24:55 -0600 Subject: [PATCH 106/298] Fix guardian being rediscovered via dhcp (#65332) --- .../components/guardian/config_flow.py | 6 +++ tests/components/guardian/test_config_flow.py | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index ea4589ddd42..c027fe8bc20 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -68,6 +68,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured( updates={CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]} ) + self._async_abort_entries_match( + {CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]} + ) else: self._abort_if_unique_id_configured() @@ -103,6 +106,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_IP_ADDRESS: discovery_info.ip, CONF_PORT: DEFAULT_PORT, } + await self._async_set_unique_id( + async_get_pin_from_uid(discovery_info.macaddress.replace(":", "").upper()) + ) return await self._async_handle_discovery() async def async_step_zeroconf( diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index b8d8a10752d..fc3157289e9 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -13,6 +13,8 @@ from homeassistant.components.guardian.config_flow import ( from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from tests.common import MockConfigEntry + async def test_duplicate_error(hass, config, config_entry, setup_guardian): """Test that errors are shown when duplicate entries are added.""" @@ -166,3 +168,45 @@ async def test_step_dhcp_already_in_progress(hass): ) assert result["type"] == "abort" assert result["reason"] == "already_in_progress" + + +async def test_step_dhcp_already_setup_match_mac(hass): + """Test we abort if the device is already setup with matching unique id and discovered via DHCP.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}, unique_id="guardian_ABCD" + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="192.168.1.100", + hostname="GVC1-ABCD.local.", + macaddress="aa:bb:cc:dd:ab:cd", + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_step_dhcp_already_setup_match_ip(hass): + """Test we abort if the device is already setup with matching ip and discovered via DHCP.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "192.168.1.100"}, + unique_id="guardian_0000", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="192.168.1.100", + hostname="GVC1-ABCD.local.", + macaddress="aa:bb:cc:dd:ab:cd", + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 3f8d2f310259e33615753309ec2ddc165c1c22cb Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 31 Jan 2022 23:43:46 +0100 Subject: [PATCH 107/298] Add diagnostics support to Fritz (#65334) * Add diagnostics support to Fritz * Temporary remove tests * coveragerc --- .coveragerc | 1 + homeassistant/components/fritz/diagnostics.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 homeassistant/components/fritz/diagnostics.py diff --git a/.coveragerc b/.coveragerc index 17a10519049..13477927e8e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -376,6 +376,7 @@ omit = homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py + homeassistant/components/fritz/diagnostics.py homeassistant/components/fritz/sensor.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py new file mode 100644 index 00000000000..4305ee4d7cb --- /dev/null +++ b/homeassistant/components/fritz/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for AVM FRITZ!Box.""" +from __future__ import annotations + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .common import AvmWrapper +from .const import DOMAIN + +TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + + diag_data = { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "device_info": { + "model": avm_wrapper.model, + "current_firmware": avm_wrapper.current_firmware, + "latest_firmware": avm_wrapper.latest_firmware, + "update_available": avm_wrapper.update_available, + "is_router": avm_wrapper.device_is_router, + "mesh_role": avm_wrapper.mesh_role, + "last_update success": avm_wrapper.last_update_success, + "last_exception": avm_wrapper.last_exception, + }, + } + + return diag_data From 5c3d4cb9a5ecc06d8dfdbf188fde8274cf496f02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jan 2022 17:27:26 -0600 Subject: [PATCH 108/298] Prevent unifiprotect from being rediscovered on UDM-PROs (#65335) --- .../components/unifiprotect/config_flow.py | 53 ++--- .../components/unifiprotect/utils.py | 20 +- tests/components/unifiprotect/__init__.py | 3 +- .../unifiprotect/test_config_flow.py | 209 +++++++++++++++++- 4 files changed, 256 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 5b5b20e7175..720a46b4659 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -36,7 +36,7 @@ from .const import ( OUTDATED_LOG_MESSAGE, ) from .discovery import async_start_discovery -from .utils import _async_short_mac, _async_unifi_mac_from_hass +from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) @@ -88,32 +88,35 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device = discovery_info mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) await self.async_set_unique_id(mac) + source_ip = discovery_info["source_ip"] + direct_connect_domain = discovery_info["direct_connect_domain"] for entry in self._async_current_entries(include_ignore=False): - if entry.unique_id != mac: - continue - new_host = None - if ( - _host_is_direct_connect(entry.data[CONF_HOST]) - and discovery_info["direct_connect_domain"] - and entry.data[CONF_HOST] != discovery_info["direct_connect_domain"] + entry_host = entry.data[CONF_HOST] + entry_has_direct_connect = _host_is_direct_connect(entry_host) + if entry.unique_id == mac: + new_host = None + if ( + entry_has_direct_connect + and direct_connect_domain + and entry_host != direct_connect_domain + ): + new_host = direct_connect_domain + elif not entry_has_direct_connect and entry_host != source_ip: + new_host = source_ip + if new_host: + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_HOST: new_host} + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + if entry_host in (direct_connect_domain, source_ip) or ( + entry_has_direct_connect + and (ip := await _async_resolve(self.hass, entry_host)) + and ip == source_ip ): - new_host = discovery_info["direct_connect_domain"] - elif ( - not _host_is_direct_connect(entry.data[CONF_HOST]) - and entry.data[CONF_HOST] != discovery_info["source_ip"] - ): - new_host = discovery_info["source_ip"] - if new_host: - self.hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_HOST: new_host} - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - return self.async_abort(reason="already_configured") - self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info["source_ip"]} - ) + return self.async_abort(reason="already_configured") return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 45645d6f06b..559cfd37660 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,10 +1,12 @@ """UniFi Protect Integration utils.""" from __future__ import annotations +import contextlib from enum import Enum +import socket from typing import Any -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback def get_nested_attr(obj: Any, attr: str) -> Any: @@ -33,3 +35,19 @@ def _async_unifi_mac_from_hass(mac: str) -> str: def _async_short_mac(mac: str) -> str: """Get the short mac address from the full mac.""" return _async_unifi_mac_from_hass(mac)[-6:] + + +async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: + """Resolve a hostname to an ip.""" + with contextlib.suppress(OSError): + return next( + iter( + raw[0] + for family, _, _, _, raw in await hass.loop.getaddrinfo( + host, None, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP + ) + if family == socket.AF_INET + ), + None, + ) + return None diff --git a/tests/components/unifiprotect/__init__.py b/tests/components/unifiprotect/__init__.py index 5fd1b7cc909..1bbe9fb435d 100644 --- a/tests/components/unifiprotect/__init__.py +++ b/tests/components/unifiprotect/__init__.py @@ -8,6 +8,7 @@ from unifi_discovery import AIOUnifiScanner, UnifiDevice, UnifiService DEVICE_HOSTNAME = "unvr" DEVICE_IP_ADDRESS = "127.0.0.1" DEVICE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +DIRECT_CONNECT_DOMAIN = "x.ui.direct" UNIFI_DISCOVERY = UnifiDevice( @@ -16,7 +17,7 @@ UNIFI_DISCOVERY = UnifiDevice( platform=DEVICE_HOSTNAME, hostname=DEVICE_HOSTNAME, services={UnifiService.Protect: True}, - direct_connect_domain="x.ui.direct", + direct_connect_domain=DIRECT_CONNECT_DOMAIN, ) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 557eb3d5e79..fc5b2b32873 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +import socket from unittest.mock import patch import pytest @@ -29,6 +30,7 @@ from . import ( DEVICE_HOSTNAME, DEVICE_IP_ADDRESS, DEVICE_MAC_ADDRESS, + DIRECT_CONNECT_DOMAIN, UNIFI_DISCOVERY, UNIFI_DISCOVERY_PARTIAL, _patch_discovery, @@ -334,7 +336,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { - "host": "x.ui.direct", + "host": DIRECT_CONNECT_DOMAIN, "username": "test-username", "password": "test-password", "id": "UnifiProtect", @@ -377,7 +379,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 1 - assert mock_config.data[CONF_HOST] == "x.ui.direct" + assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_using_direct_connect( @@ -518,3 +520,206 @@ async def test_discovered_by_unifi_discovery_partial( "verify_ssl": False, } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery from an alternate interface.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": DIRECT_CONNECT_DOMAIN, + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + }, + unique_id="FFFFFFAAAAAA", + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_ip_matches( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery from an alternate interface when the ip matches.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "127.0.0.1", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + }, + unique_id="FFFFFFAAAAAA", + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolves to host ip.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "y.ui.direct", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + }, + unique_id="FFFFFFAAAAAA", + ) + mock_config.add_to_hass(hass) + + other_ip_dict = UNIFI_DISCOVERY_DICT.copy() + other_ip_dict["source_ip"] = "127.0.0.1" + other_ip_dict["direct_connect_domain"] = "nomatchsameip.ui.direct" + + with _patch_discovery(), patch.object( + hass.loop, + "getaddrinfo", + return_value=[(socket.AF_INET, None, None, None, ("127.0.0.1", 443))], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=other_ip_dict, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_fails( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test we can still configure if the resolver fails.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "y.ui.direct", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + }, + unique_id="FFFFFFAAAAAA", + ) + mock_config.add_to_hass(hass) + + other_ip_dict = UNIFI_DISCOVERY_DICT.copy() + other_ip_dict["source_ip"] = "127.0.0.2" + other_ip_dict["direct_connect_domain"] = "nomatchsameip.ui.direct" + + with _patch_discovery(), patch.object( + hass.loop, "getaddrinfo", side_effect=OSError + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=other_ip_dict, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert flows[0]["context"]["title_placeholders"] == { + "ip_address": "127.0.0.2", + "name": "unvr", + } + + assert not result["errors"] + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + return_value=mock_nvr, + ), patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "UnifiProtect" + assert result2["data"] == { + "host": "nomatchsameip.ui.direct", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_no_result( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolve has no result.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "y.ui.direct", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + }, + unique_id="FFFFFFAAAAAA", + ) + mock_config.add_to_hass(hass) + + other_ip_dict = UNIFI_DISCOVERY_DICT.copy() + other_ip_dict["source_ip"] = "127.0.0.2" + other_ip_dict["direct_connect_domain"] = "y.ui.direct" + + with _patch_discovery(), patch.object(hass.loop, "getaddrinfo", return_value=[]): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=other_ip_dict, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 114da0bd4fddf8548266c9700705a8cc720abcfc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Jan 2022 15:52:31 -0800 Subject: [PATCH 109/298] Bump version tag on async_timeout warning (#65339) --- homeassistant/async_timeout_backcompat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/async_timeout_backcompat.py b/homeassistant/async_timeout_backcompat.py index 70b38c18708..212beddfae3 100644 --- a/homeassistant/async_timeout_backcompat.py +++ b/homeassistant/async_timeout_backcompat.py @@ -17,7 +17,7 @@ def timeout( loop = asyncio.get_running_loop() else: report( - "called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.2", + "called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.3", error_if_core=False, ) if delay is not None: @@ -30,7 +30,7 @@ def timeout( def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task[Any] | None: """Backwards compatible current_task.""" report( - "called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.2; use asyncio.current_task instead", + "called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.3; use asyncio.current_task instead", error_if_core=False, ) return asyncio.current_task() From 90127d04fa1e4a8fcba95bcfa51896aa4f4072e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Jan 2022 15:58:52 -0800 Subject: [PATCH 110/298] Bump aiohue to 4.0.1 (#65340) --- homeassistant/components/hue/bridge.py | 7 ++++--- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/migration.py | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 3a529c15cf3..346cc67d235 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -49,11 +49,12 @@ class HueBridge: self.logger = logging.getLogger(__name__) # store actual api connection to bridge as api app_key: str = self.config_entry.data[CONF_API_KEY] - websession = aiohttp_client.async_get_clientsession(hass) if self.api_version == 1: - self.api = HueBridgeV1(self.host, app_key, websession) + self.api = HueBridgeV1( + self.host, app_key, aiohttp_client.async_get_clientsession(hass) + ) else: - self.api = HueBridgeV2(self.host, app_key, websession) + self.api = HueBridgeV2(self.host, app_key) # store (this) bridge object in hass data hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 832592f3f1b..12f7df13866 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.11"], + "requirements": ["aiohue==4.0.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 3dbfef42d16..f779fccdb3b 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -76,7 +76,6 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N """Perform migration of devices and entities to V2 Id's.""" host = entry.data[CONF_HOST] api_key = entry.data[CONF_API_KEY] - websession = aiohttp_client.async_get_clientsession(hass) dev_reg = async_get_device_registry(hass) ent_reg = async_get_entity_registry(hass) LOGGER.info("Start of migration of devices and entities to support API schema 2") @@ -93,7 +92,7 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N dev_ids[normalized_mac] = hass_dev.id # initialize bridge connection just for the migration - async with HueBridgeV2(host, api_key, websession) as api: + async with HueBridgeV2(host, api_key) as api: sensor_class_mapping = { SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER, diff --git a/requirements_all.txt b/requirements_all.txt index bb44e203352..81c089bfaa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.11 +aiohue==4.0.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adf7c1dd3c0..ccac691eaa9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.11 +aiohue==4.0.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 From 5735762af2ec9ee641093d61f2a7f89b5d9a06ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jan 2022 18:08:42 -0600 Subject: [PATCH 111/298] Bump zeroconf to 0.38.3 (#65341) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 16a8a8ff26e..6f6a56774d7 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.38.1"], + "requirements": ["zeroconf==0.38.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 44486677384..15c1afc1b99 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.7.2 -zeroconf==0.38.1 +zeroconf==0.38.3 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 81c089bfaa7..2b76a33daa4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2531,7 +2531,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.1 +zeroconf==0.38.3 # homeassistant.components.zha zha-quirks==0.0.66 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccac691eaa9..bc934e32499 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1550,7 +1550,7 @@ yeelight==0.7.8 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.1 +zeroconf==0.38.3 # homeassistant.components.zha zha-quirks==0.0.66 From 63a90b722645cb4690f488782cfdb04b0109d333 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 1 Feb 2022 01:10:55 +0100 Subject: [PATCH 112/298] Add diagnostics for SamsungTV (#65342) --- .coveragerc | 1 + .../components/samsungtv/diagnostics.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 homeassistant/components/samsungtv/diagnostics.py diff --git a/.coveragerc b/.coveragerc index 13477927e8e..6f410b62cf1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -947,6 +947,7 @@ omit = homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py homeassistant/components/samsungtv/bridge.py + homeassistant/components/samsungtv/diagnostics.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py new file mode 100644 index 00000000000..18d2325f38c --- /dev/null +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -0,0 +1,18 @@ +"""Diagnostics support for SamsungTV.""" +from __future__ import annotations + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TOKEN +from homeassistant.core import HomeAssistant + +TO_REDACT = {CONF_TOKEN} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + diag_data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} + + return diag_data From b7c7571a39e2d6f9dc1af7c760770fab72476e1c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Jan 2022 17:04:46 -0800 Subject: [PATCH 113/298] I zone, you zone, we zoning (#65344) --- homeassistant/components/zone/__init__.py | 66 +------------ tests/components/zone/test_init.py | 115 +--------------------- 2 files changed, 7 insertions(+), 174 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 41fdd8c32d3..dd327acbf75 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,7 +1,6 @@ """Support for the definition of zones.""" from __future__ import annotations -from collections.abc import Callable import logging from typing import Any, cast @@ -10,7 +9,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( ATTR_EDITABLE, - ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ICON, @@ -29,7 +27,6 @@ from homeassistant.helpers import ( config_validation as cv, entity, entity_component, - event, service, storage, ) @@ -287,10 +284,7 @@ class Zone(entity.Entity): """Initialize the zone.""" self._config = config self.editable = True - self._attrs: dict | None = None - self._remove_listener: Callable[[], None] | None = None self._generate_attrs() - self._persons_in_zone: set[str] = set() @classmethod def from_yaml(cls, config: dict) -> Zone: @@ -301,9 +295,9 @@ class Zone(entity.Entity): return zone @property - def state(self) -> int: + def state(self) -> str: """Return the state property really does nothing for a zone.""" - return len(self._persons_in_zone) + return "zoning" @property def name(self) -> str: @@ -320,11 +314,6 @@ class Zone(entity.Entity): """Return the icon if any.""" return self._config.get(CONF_ICON) - @property - def extra_state_attributes(self) -> dict | None: - """Return the state attributes of the zone.""" - return self._attrs - @property def should_poll(self) -> bool: """Zone does not poll.""" @@ -338,59 +327,10 @@ class Zone(entity.Entity): self._generate_attrs() self.async_write_ha_state() - @callback - def _person_state_change_listener(self, evt: Event) -> None: - person_entity_id = evt.data["entity_id"] - cur_count = len(self._persons_in_zone) - if ( - (state := evt.data["new_state"]) - and (latitude := state.attributes.get(ATTR_LATITUDE)) is not None - and (longitude := state.attributes.get(ATTR_LONGITUDE)) is not None - and (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is not None - and ( - zone_state := async_active_zone( - self.hass, latitude, longitude, accuracy - ) - ) - and zone_state.entity_id == self.entity_id - ): - self._persons_in_zone.add(person_entity_id) - elif person_entity_id in self._persons_in_zone: - self._persons_in_zone.remove(person_entity_id) - - if len(self._persons_in_zone) != cur_count: - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - person_domain = "person" # avoid circular import - persons = self.hass.states.async_entity_ids(person_domain) - for person in persons: - state = self.hass.states.get(person) - if ( - state is None - or (latitude := state.attributes.get(ATTR_LATITUDE)) is None - or (longitude := state.attributes.get(ATTR_LONGITUDE)) is None - or (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is None - ): - continue - zone_state = async_active_zone(self.hass, latitude, longitude, accuracy) - if zone_state is not None and zone_state.entity_id == self.entity_id: - self._persons_in_zone.add(person) - - self.async_on_remove( - event.async_track_state_change_filtered( - self.hass, - event.TrackStates(False, set(), {person_domain}), - self._person_state_change_listener, - ).async_remove - ) - @callback def _generate_attrs(self) -> None: """Generate new attrs based on config.""" - self._attrs = { + self._attr_extra_state_attributes = { ATTR_LATITUDE: self._config[CONF_LATITUDE], ATTR_LONGITUDE: self._config[CONF_LONGITUDE], ATTR_RADIUS: self._config[CONF_RADIUS], diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 54cb87aa772..8d0fddb921c 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -316,7 +316,7 @@ async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.state == "0" + assert state.state == "zoning" assert state.name == "from storage" assert state.attributes.get(ATTR_EDITABLE) @@ -328,12 +328,12 @@ async def test_editable_state_attribute(hass, storage_setup): ) state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.state == "0" + assert state.state == "zoning" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) state = hass.states.get(f"{DOMAIN}.yaml_option") - assert state.state == "0" + assert state.state == "zoning" assert not state.attributes.get(ATTR_EDITABLE) @@ -457,7 +457,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): assert resp["success"] state = hass.states.get(input_entity_id) - assert state.state == "0" + assert state.state == "zoning" assert state.attributes["latitude"] == 3 assert state.attributes["longitude"] == 4 assert state.attributes["passive"] is True @@ -503,110 +503,3 @@ async def test_unavailable_zone(hass): assert zone.async_active_zone(hass, 0.0, 0.01) is None assert zone.in_zone(hass.states.get("zone.bla"), 0, 0) is False - - -async def test_state(hass): - """Test the state of a zone.""" - info = { - "name": "Test Zone", - "latitude": 32.880837, - "longitude": -117.237561, - "radius": 250, - "passive": False, - } - assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) - - assert len(hass.states.async_entity_ids("zone")) == 2 - state = hass.states.get("zone.test_zone") - assert state.state == "0" - - # Person entity enters zone - hass.states.async_set( - "person.person1", - "Test Zone", - {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, - ) - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "1" - assert hass.states.get("zone.home").state == "0" - - # Person entity enters zone - hass.states.async_set( - "person.person2", - "Test Zone", - {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, - ) - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "2" - assert hass.states.get("zone.home").state == "0" - - # Person entity enters another zone - hass.states.async_set( - "person.person1", - "home", - {"latitude": 32.87336, "longitude": -117.22743, "gps_accuracy": 0}, - ) - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "1" - assert hass.states.get("zone.home").state == "1" - - # Person entity removed - hass.states.async_remove("person.person2") - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "0" - assert hass.states.get("zone.home").state == "1" - - -async def test_state_2(hass): - """Test the state of a zone.""" - hass.states.async_set("person.person1", "unknown") - hass.states.async_set("person.person2", "unknown") - - info = { - "name": "Test Zone", - "latitude": 32.880837, - "longitude": -117.237561, - "radius": 250, - "passive": False, - } - assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) - - assert len(hass.states.async_entity_ids("zone")) == 2 - state = hass.states.get("zone.test_zone") - assert state.state == "0" - - # Person entity enters zone - hass.states.async_set( - "person.person1", - "Test Zone", - {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, - ) - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "1" - assert hass.states.get("zone.home").state == "0" - - # Person entity enters zone - hass.states.async_set( - "person.person2", - "Test Zone", - {"latitude": 32.880837, "longitude": -117.237561, "gps_accuracy": 0}, - ) - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "2" - assert hass.states.get("zone.home").state == "0" - - # Person entity enters another zone - hass.states.async_set( - "person.person1", - "home", - {"latitude": 32.87336, "longitude": -117.22743, "gps_accuracy": 0}, - ) - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "1" - assert hass.states.get("zone.home").state == "1" - - # Person entity removed - hass.states.async_remove("person.person2") - await hass.async_block_till_done() - assert hass.states.get("zone.test_zone").state == "0" - assert hass.states.get("zone.home").state == "1" From 5082582769a77ed05396db02d04079b0754ea838 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Jan 2022 17:12:40 -0800 Subject: [PATCH 114/298] Bumped version to 2022.2.0b5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 842268f7e0d..69d250c293c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 5f6efe18517..a5805ac30ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.0b4 +version = 2022.2.0b5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 7fe1b85495c3112fecb5c78764be87e07b6282bf Mon Sep 17 00:00:00 2001 From: ZuluWhiskey <35011199+ZuluWhiskey@users.noreply.github.com> Date: Tue, 1 Feb 2022 17:05:50 +0000 Subject: [PATCH 115/298] Fix MotionEye config flow (#64360) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/motioneye/config_flow.py | 20 +++++++++---------- .../components/motioneye/test_config_flow.py | 18 +++++++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 84a6d0771e6..0361f4562c4 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -222,17 +222,15 @@ class MotionEyeOptionsFlow(OptionsFlow): if self.show_advanced_options: # The input URL is not validated as being a URL, to allow for the possibility - # the template input won't be a valid URL until after it's rendered. - schema.update( - { - vol.Required( - CONF_STREAM_URL_TEMPLATE, - default=self._config_entry.options.get( - CONF_STREAM_URL_TEMPLATE, - "", - ), - ): str + # the template input won't be a valid URL until after it's rendered + stream_kwargs = {} + if CONF_STREAM_URL_TEMPLATE in self._config_entry.options: + stream_kwargs["description"] = { + "suggested_value": self._config_entry.options[ + CONF_STREAM_URL_TEMPLATE + ] } - ) + + schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, **stream_kwargs)] = str return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 57def069d59..9ef0f78874d 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -480,6 +480,24 @@ async def test_advanced_options(hass: HomeAssistant) -> None: ) as mock_setup_entry: await hass.async_block_till_done() + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_WEBHOOK_SET: True, + CONF_WEBHOOK_SET_OVERWRITE: True, + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_WEBHOOK_SET] + assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] + assert CONF_STREAM_URL_TEMPLATE not in result["data"] + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) From 09c2c129b93d4f3abf526ccccfd545965c8f1208 Mon Sep 17 00:00:00 2001 From: schreyack Date: Tue, 1 Feb 2022 09:11:09 -0800 Subject: [PATCH 116/298] Fix honeywell hold mode (#65327) Co-authored-by: Martin Hjelmare --- homeassistant/components/honeywell/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 57ce0125c93..f0e18953402 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -242,7 +242,7 @@ class HoneywellUSThermostat(ClimateEntity): # Get current mode mode = self._device.system_mode # Set hold if this is not the case - if getattr(self._device, f"hold_{mode}") is False: + if getattr(self._device, f"hold_{mode}", None) is False: # Get next period key next_period_key = f"{mode.capitalize()}NextPeriod" # Get next period raw value From 68651be2ccf990fb4a4080b5ce84f763aec06cae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Jan 2022 22:22:12 -0800 Subject: [PATCH 117/298] Simplify unifi cleanup logic (#65345) --- .../components/unifi/unifi_entity_base.py | 30 +++---------- tests/components/unifi/test_device_tracker.py | 45 +------------------ 2 files changed, 6 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 1c3251d213c..c611fc1ee60 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -3,7 +3,7 @@ import logging from typing import Any from homeassistant.core import callback -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -78,34 +78,14 @@ class UniFiBase(Entity): raise NotImplementedError async def remove_item(self, keys: set) -> None: - """Remove entity if key is part of set. - - Remove entity if no entry in entity registry exist. - Remove entity registry entry if no entry in device registry exist. - Remove device registry entry if there is only one linked entity (this entity). - Remove config entry reference from device registry entry if there is more than one config entry. - Remove entity registry entry if there are more than one entity linked to the device registry entry. - """ + """Remove entity if key is part of set.""" if self.key not in keys: return - entity_registry = er.async_get(self.hass) - entity_entry = entity_registry.async_get(self.entity_id) - if not entity_entry: + if self.registry_entry: + er.async_get(self.hass).async_remove(self.entity_id) + else: await self.async_remove(force_remove=True) - return - - device_registry = dr.async_get(self.hass) - device_entry = device_registry.async_get(entity_entry.device_id) - if not device_entry: - entity_registry.async_remove(self.entity_id) - return - - device_registry.async_update_device( - entity_entry.device_id, - remove_config_entry_id=self.controller.config_entry.entry_id, - ) - entity_registry.async_remove(self.entity_id) @property def should_poll(self) -> bool: diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index cf4861980a0..b490d43fffd 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -18,7 +18,7 @@ from homeassistant.components.unifi.const import ( DOMAIN as UNIFI_DOMAIN, ) from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .test_controller import ENTRY_CONFIG, setup_unifi_integration @@ -317,49 +317,6 @@ async def test_remove_clients( assert hass.states.get("device_tracker.client_2") -async def test_remove_client_but_keep_device_entry( - hass, aioclient_mock, mock_unifi_websocket, mock_device_registry -): - """Test that unifi entity base remove config entry id from a multi integration device registry entry.""" - client_1 = { - "essid": "ssid", - "hostname": "client_1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - await setup_unifi_integration(hass, aioclient_mock, clients_response=[client_1]) - - device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_or_create( - config_entry_id="other", - connections={("mac", "00:00:00:00:00:01")}, - ) - - entity_registry = er.async_get(hass) - other_entity = entity_registry.async_get_or_create( - TRACKER_DOMAIN, - "other", - "unique_id", - device_id=device_entry.id, - ) - assert len(device_entry.config_entries) == 3 - - mock_unifi_websocket( - data={ - "meta": {"message": MESSAGE_CLIENT_REMOVED}, - "data": [client_1], - } - ) - await hass.async_block_till_done() - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 0 - - device_entry = device_registry.async_get(other_entity.device_id) - assert len(device_entry.config_entries) == 2 - - async def test_controller_state_change( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry ): From 055382c84cc7409e5135ca0291400d43c33790d9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Feb 2022 10:47:12 +0100 Subject: [PATCH 118/298] Improve CastProtocol (#65357) * Improve CastProtocol * Tweak --- homeassistant/components/cast/__init__.py | 2 +- homeassistant/components/cast/media_player.py | 2 +- homeassistant/components/plex/cast.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index cb631e17ccd..86e5557160c 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -67,7 +67,7 @@ class CastProtocol(Protocol): """Define the format of cast platforms.""" async def async_get_media_browser_root_object( - self, cast_type: str + self, hass: HomeAssistant, cast_type: str ) -> list[BrowseMedia]: """Create a list of root objects for media browsing.""" diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 5f3381896da..975fa3f5836 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -463,7 +463,7 @@ class CastDevice(MediaPlayerEntity): for platform in self.hass.data[CAST_DOMAIN].values(): children.extend( await platform.async_get_media_browser_root_object( - self._chromecast.cast_type + self.hass, self._chromecast.cast_type ) ) diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index c2a09ff8810..59f23a681f8 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -14,7 +14,9 @@ from .const import PLEX_URI_SCHEME from .services import lookup_plex_media -async def async_get_media_browser_root_object(cast_type: str) -> list[BrowseMedia]: +async def async_get_media_browser_root_object( + hass: HomeAssistant, cast_type: str +) -> list[BrowseMedia]: """Create a root object for media browsing.""" return [ BrowseMedia( From 03bd3f500158ea41dcf60d74fe233233ce204eff Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 1 Feb 2022 16:47:42 +0100 Subject: [PATCH 119/298] Fix options for dnsip (#65369) --- homeassistant/components/dnsip/config_flow.py | 6 ++- homeassistant/components/dnsip/sensor.py | 6 +-- tests/components/dnsip/test_config_flow.py | 45 ++++++++++++++----- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index e47dc67d58d..bedcc5f821c 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -110,11 +110,13 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={ CONF_HOSTNAME: hostname, CONF_NAME: name, - CONF_RESOLVER: resolver, - CONF_RESOLVER_IPV6: resolver_ipv6, CONF_IPV4: validate[CONF_IPV4], CONF_IPV6: validate[CONF_IPV6], }, + options={ + CONF_RESOLVER: resolver, + CONF_RESOLVER_IPV6: resolver_ipv6, + }, ) return self.async_show_form( diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 9057f3e8c33..7dfc3aaa544 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -79,10 +79,8 @@ async def async_setup_entry( hostname = entry.data[CONF_HOSTNAME] name = entry.data[CONF_NAME] - resolver_ipv4 = entry.options.get(CONF_RESOLVER, entry.data[CONF_RESOLVER]) - resolver_ipv6 = entry.options.get( - CONF_RESOLVER_IPV6, entry.data[CONF_RESOLVER_IPV6] - ) + resolver_ipv4 = entry.options[CONF_RESOLVER] + resolver_ipv6 = entry.options[CONF_RESOLVER_IPV6] entities = [] if entry.data[CONF_IPV4]: entities.append(WanIpSensor(name, hostname, resolver_ipv4, False)) diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 3ebbdfe91da..59dcb81aa94 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -69,11 +69,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["data"] == { "hostname": "home-assistant.io", "name": "home-assistant.io", - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", "ipv4": True, "ipv6": True, } + assert result2["options"] == { + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:0:ccc::2", + } assert len(mock_setup_entry.mock_calls) == 1 @@ -101,34 +103,41 @@ async def test_form_error(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "p_input,p_output", + "p_input,p_output,p_options", [ ( {CONF_HOSTNAME: "home-assistant.io"}, { "hostname": "home-assistant.io", "name": "home-assistant.io", - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", "ipv4": True, "ipv6": True, }, + { + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:0:ccc::2", + }, ), ( {}, { "hostname": "myip.opendns.com", "name": "myip", - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", "ipv4": True, "ipv6": True, }, + { + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:0:ccc::2", + }, ), ], ) async def test_import_flow_success( - hass: HomeAssistant, p_input: dict[str, str], p_output: dict[str, str] + hass: HomeAssistant, + p_input: dict[str, str], + p_output: dict[str, str], + p_options: dict[str, str], ) -> None: """Test a successful import of YAML.""" @@ -149,6 +158,7 @@ async def test_import_flow_success( assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == p_output["name"] assert result2["data"] == p_output + assert result2["options"] == p_options assert len(mock_setup_entry.mock_calls) == 1 @@ -160,11 +170,13 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: data={ CONF_HOSTNAME: "home-assistant.io", CONF_NAME: "home-assistant.io", - CONF_RESOLVER: "208.67.222.222", - CONF_RESOLVER_IPV6: "2620:0:ccc::2", CONF_IPV4: True, CONF_IPV6: True, }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + }, unique_id="home-assistant.io", ).add_to_hass(hass) @@ -199,11 +211,13 @@ async def test_options_flow(hass: HomeAssistant) -> None: data={ CONF_HOSTNAME: "home-assistant.io", CONF_NAME: "home-assistant.io", - CONF_RESOLVER: "208.67.222.222", - CONF_RESOLVER_IPV6: "2620:0:ccc::2", CONF_IPV4: True, CONF_IPV6: False, }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + }, ) entry.add_to_hass(hass) @@ -267,6 +281,13 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No ) entry.add_to_hass(hass) + with patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(entry.entry_id) with patch( From 4f8752b3519d0102fc722961fd0c28dfe82ff0b6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Feb 2022 18:45:08 +0100 Subject: [PATCH 120/298] Allow removing keys from automation (#65374) --- homeassistant/components/config/automation.py | 37 ++++++++----------- homeassistant/components/config/scene.py | 2 +- tests/components/config/test_automation.py | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 01e22297c0d..f1a2d9aab84 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,5 +1,4 @@ """Provide configuration end points for Automations.""" -from collections import OrderedDict import uuid from homeassistant.components.automation.config import ( @@ -52,7 +51,18 @@ class EditAutomationConfigView(EditIdBasedConfigView): def _write_value(self, hass, data, config_key, new_value): """Set value.""" - index = None + updated_value = {CONF_ID: config_key} + + # Iterate through some keys that we want to have ordered in the output + for key in ("alias", "description", "trigger", "condition", "action"): + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(new_value) + + updated = False for index, cur_value in enumerate(data): # When people copy paste their automations to the config file, # they sometimes forget to add IDs. Fix it here. @@ -60,23 +70,8 @@ class EditAutomationConfigView(EditIdBasedConfigView): cur_value[CONF_ID] = uuid.uuid4().hex elif cur_value[CONF_ID] == config_key: - break - else: - cur_value = OrderedDict() - cur_value[CONF_ID] = config_key - index = len(data) - data.append(cur_value) + data[index] = updated_value + updated = True - # Iterate through some keys that we want to have ordered in the output - updated_value = OrderedDict() - for key in ("id", "alias", "description", "trigger", "condition", "action"): - if key in cur_value: - updated_value[key] = cur_value[key] - if key in new_value: - updated_value[key] = new_value[key] - - # We cover all current fields above, but just in case we start - # supporting more fields in the future. - updated_value.update(cur_value) - updated_value.update(new_value) - data[index] = updated_value + if not updated: + data.append(updated_value) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 41b8dce0957..6523ff84158 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -47,8 +47,8 @@ class EditSceneConfigView(EditIdBasedConfigView): def _write_value(self, hass, data, config_key, new_value): """Set value.""" - # Iterate through some keys that we want to have ordered in the output updated_value = {CONF_ID: config_key} + # Iterate through some keys that we want to have ordered in the output for key in ("name", "entities"): if key in new_value: updated_value[key] = new_value[key] diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 80ee38350aa..6f782fdbbff 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -80,6 +80,43 @@ async def test_update_device_config(hass, hass_client, setup_automation): assert written[0] == orig_data +@pytest.mark.parametrize("automation_config", ({},)) +async def test_update_remove_key_device_config(hass, hass_client, setup_automation): + """Test updating device config while removing a key.""" + with patch.object(config, "SECTIONS", ["automation"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [{"id": "sun", "key": "value"}, {"id": "moon", "key": "value"}] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): + resp = await client.post( + "/api/config/automation/config/moon", + data=json.dumps({"trigger": [], "action": [], "condition": []}), + ) + + assert resp.status == HTTPStatus.OK + result = await resp.json() + assert result == {"result": "ok"} + + assert list(orig_data[1]) == ["id", "trigger", "condition", "action"] + assert orig_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} + assert written[0] == orig_data + + @pytest.mark.parametrize("automation_config", ({},)) async def test_bad_formatted_automations(hass, hass_client, setup_automation): """Test that we handle automations without ID.""" From 19fff6489b9176dbe3fa1dd5202b4d791a47e96b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 1 Feb 2022 18:57:34 +0100 Subject: [PATCH 121/298] Fix wan_access switch for disconnected devices in Fritz!Tools (#65378) --- homeassistant/components/fritz/common.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index c9eff204bf2..dede4b82c02 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -372,13 +372,14 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): dev_info: Device = hosts[dev_mac] + if dev_info.ip_address: + dev_info.wan_access = self._get_wan_access(dev_info.ip_address) + for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) if intf is not None: - if intf["op_mode"] != "AP_GUEST" and dev_info.ip_address: - dev_info.wan_access = self._get_wan_access( - dev_info.ip_address - ) + if intf["op_mode"] == "AP_GUEST": + dev_info.wan_access = None dev_info.connected_to = intf["device"] dev_info.connection_type = intf["type"] From f3c39d8dcad6a4f33bbd6fbc7bea4afa4a8d4834 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 1 Feb 2022 18:52:56 +0100 Subject: [PATCH 122/298] Redact host address in UniFi diagnostics (#65379) --- homeassistant/components/unifi/diagnostics.py | 4 ++-- tests/components/unifi/test_diagnostics.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 4f27ff4ff4f..ed059856881 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -7,14 +7,14 @@ from typing import Any from homeassistant.components.diagnostics import REDACTED, async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} -REDACT_CONFIG = {CONF_CONTROLLER, CONF_PASSWORD, CONF_USERNAME} +REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME} REDACT_CLIENTS = {"bssid", "essid"} REDACT_DEVICES = { "anon_id", diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 3de9393e5b9..ccec7fcb48a 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -122,7 +122,7 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): "config": { "data": { "controller": REDACTED, - "host": "1.2.3.4", + "host": REDACTED, "password": REDACTED, "port": 1234, "site": "site_id", From b687f68d53d449a6bca1be69d8be5898b4260460 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Feb 2022 09:58:23 -0800 Subject: [PATCH 123/298] Bump frontend to 20220201.0 (#65380) --- 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 1eef7aff083..49e49ac2efa 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220127.0" + "home-assistant-frontend==20220201.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 15c1afc1b99..30086a676e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.6.3 hass-nabucasa==0.52.0 -home-assistant-frontend==20220127.0 +home-assistant-frontend==20220201.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2b76a33daa4..66977136936 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220127.0 +home-assistant-frontend==20220201.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc934e32499..3b48abc36b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220127.0 +home-assistant-frontend==20220201.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From ba237fd3838c22780f54fb6ff2dae30a18b4a16e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Feb 2022 10:00:39 -0800 Subject: [PATCH 124/298] Bumped version to 2022.2.0b6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 69d250c293c..db26c8170b3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index a5805ac30ac..18765d7e8e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.0b5 +version = 2022.2.0b6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From b902c595041cfd633be494cd322962bebedf98e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Feb 2022 15:06:27 +0100 Subject: [PATCH 125/298] Report unmet dependencies for failing config flows (#65061) * Report unmet dependencies for failing config flows * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update homeassistant/setup.py Co-authored-by: Martin Hjelmare * Modify error message * Add test Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- .../components/config/config_entries.py | 11 +++++-- homeassistant/exceptions.py | 12 ++++++++ homeassistant/setup.py | 18 +++++++----- .../components/config/test_config_entries.py | 29 +++++++++++++++++++ 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 1e26a9c46d4..887c0517d05 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus +from aiohttp import web import aiohttp.web_exceptions import voluptuous as vol @@ -11,7 +12,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import DependencyError, Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, @@ -127,7 +128,13 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") # pylint: disable=no-value-for-parameter - return await super().post(request) + try: + return await super().post(request) + except DependencyError as exc: + return web.Response( + text=f"Failed dependencies {', '.join(exc.failed_dependencies)}", + status=HTTPStatus.BAD_REQUEST, + ) def _prepare_result_json(self, result): """Convert result to JSON.""" diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index ff8fb295cd5..052d3de4768 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -199,3 +199,15 @@ class RequiredParameterMissing(HomeAssistantError): ), ) self.parameter_names = parameter_names + + +class DependencyError(HomeAssistantError): + """Raised when dependencies can not be setup.""" + + def __init__(self, failed_dependencies: list[str]) -> None: + """Initialize error.""" + super().__init__( + self, + f"Could not setup dependencies: {', '.join(failed_dependencies)}", + ) + self.failed_dependencies = failed_dependencies diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5ff6519f6ec..5c56cb55b19 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -18,7 +18,7 @@ from .const import ( Platform, ) from .core import CALLBACK_TYPE -from .exceptions import HomeAssistantError +from .exceptions import DependencyError, HomeAssistantError from .helpers.typing import ConfigType from .util import dt as dt_util, ensure_unique_string @@ -83,8 +83,11 @@ async def async_setup_component( async def _async_process_dependencies( hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration -) -> bool: - """Ensure all dependencies are set up.""" +) -> list[str]: + """Ensure all dependencies are set up. + + Returns a list of dependencies which failed to set up. + """ dependencies_tasks = { dep: hass.loop.create_task(async_setup_component(hass, dep, config)) for dep in integration.dependencies @@ -104,7 +107,7 @@ async def _async_process_dependencies( ) if not dependencies_tasks and not after_dependencies_tasks: - return True + return [] if dependencies_tasks: _LOGGER.debug( @@ -135,8 +138,7 @@ async def _async_process_dependencies( ", ".join(failed), ) - return False - return True + return failed async def _async_setup_component( @@ -341,8 +343,8 @@ async def async_process_deps_reqs( elif integration.domain in processed: return - if not await _async_process_dependencies(hass, config, integration): - raise HomeAssistantError("Could not set up all dependencies.") + if failed_deps := await _async_process_dependencies(hass, config, integration): + raise DependencyError(failed_deps) if not hass.config.skip_pip and integration.requirements: async with hass.timeout.async_freeze(integration.domain): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 7f88d9b482b..6608bf3471d 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -252,6 +252,35 @@ async def test_initialize_flow(hass, client): } +async def test_initialize_flow_unmet_dependency(hass, client): + """Test unmet dependencies are listed.""" + mock_entity_platform(hass, "config_flow.test", None) + + config_schema = vol.Schema({"comp_conf": {"hello": str}}, required=True) + mock_integration( + hass, MockModule(domain="dependency_1", config_schema=config_schema) + ) + # The test2 config flow should fail because dependency_1 can't be automatically setup + mock_integration( + hass, + MockModule(domain="test2", partial_manifest={"dependencies": ["dependency_1"]}), + ) + + class TestFlow(core_ce.ConfigFlow): + async def async_step_user(self, user_input=None): + pass + + with patch.dict(HANDLERS, {"test2": TestFlow}): + resp = await client.post( + "/api/config/config_entries/flow", + json={"handler": "test2", "show_advanced_options": True}, + ) + + assert resp.status == HTTPStatus.BAD_REQUEST + data = await resp.text() + assert data == "Failed dependencies dependency_1" + + async def test_initialize_flow_unauth(hass, client, hass_admin_user): """Test we can initialize a flow.""" hass_admin_user.groups = [] From 37f9c833c056924f9fc4266eaf27f0d6a417d673 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 2 Feb 2022 16:14:52 +0100 Subject: [PATCH 126/298] Fix MQTT expire_after effects after reloading (#65359) * Cleanup sensor expire triggers after reload * fix test binary_sensor * Also trigger cleanup parent classes * Restore an expiring state after a reload * correct discovery_update * restore expiring state with remaining time * Update homeassistant/components/mqtt/binary_sensor.py description Co-authored-by: Erik Montnemery * Log remaining time * Move check * check and tests reload * remove self.async_write_ha_state() Co-authored-by: Erik Montnemery --- .../components/mqtt/binary_sensor.py | 41 +++++++- homeassistant/components/mqtt/mixins.py | 5 + homeassistant/components/mqtt/sensor.py | 41 +++++++- tests/components/mqtt/test_binary_sensor.py | 91 +++++++++++++++++- tests/components/mqtt/test_common.py | 36 ++++--- tests/components/mqtt/test_sensor.py | 93 ++++++++++++++++++- 6 files changed, 289 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 800db2cad79..e26fe0b0259 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -20,6 +20,8 @@ from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -27,6 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -95,7 +98,7 @@ async def _async_setup_entity( async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) -class MqttBinarySensor(MqttEntity, BinarySensorEntity): +class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Representation a binary sensor that is updated by MQTT.""" _entity_id_format = binary_sensor.ENTITY_ID_FORMAT @@ -113,6 +116,42 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + async def async_added_to_hass(self) -> None: + """Restore state for entities with expire_after set.""" + await super().async_added_to_hass() + if ( + (expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None + and expire_after > 0 + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ): + expiration_at = last_state.last_changed + timedelta(seconds=expire_after) + if expiration_at < (time_now := dt_util.utcnow()): + # Skip reactivating the binary_sensor + _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) + return + self._expired = False + self._state = last_state.state + + self._expiration_trigger = async_track_point_in_utc_time( + self.hass, self._value_is_expired, expiration_at + ) + _LOGGER.debug( + "State recovered after reload for %s, remaining time before expiring %s", + self.entity_id, + expiration_at - time_now, + ) + + async def async_will_remove_from_hass(self) -> None: + """Remove exprire triggers.""" + # Clean up expire triggers + if self._expiration_trigger: + _LOGGER.debug("Clean up expire after trigger for %s", self.entity_id) + self._expiration_trigger() + self._expiration_trigger = None + self._expired = False + await MqttEntity.async_will_remove_from_hass(self) + @staticmethod def config_schema(): """Return the config schema.""" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index a150a0cf49c..29676e4c9b9 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -523,6 +523,11 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" if not self._removed_from_hass: + # Stop subscribing to discovery updates to not trigger when we clear the + # discovery topic + self._cleanup_discovery_on_remove() + + # Clear the discovery topic so the entity is not rediscovered after a restart discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] publish(self.hass, discovery_topic, "", retain=True) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index c457cff5d09..59f124155d3 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -23,12 +23,15 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -140,7 +143,7 @@ async def _async_setup_entity( async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) -class MqttSensor(MqttEntity, SensorEntity): +class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): """Representation of a sensor that can be updated using MQTT.""" _entity_id_format = ENTITY_ID_FORMAT @@ -160,6 +163,42 @@ class MqttSensor(MqttEntity, SensorEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + async def async_added_to_hass(self) -> None: + """Restore state for entities with expire_after set.""" + await super().async_added_to_hass() + if ( + (expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None + and expire_after > 0 + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ): + expiration_at = last_state.last_changed + timedelta(seconds=expire_after) + if expiration_at < (time_now := dt_util.utcnow()): + # Skip reactivating the sensor + _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) + return + self._expired = False + self._state = last_state.state + + self._expiration_trigger = async_track_point_in_utc_time( + self.hass, self._value_is_expired, expiration_at + ) + _LOGGER.debug( + "State recovered after reload for %s, remaining time before expiring %s", + self.entity_id, + expiration_at - time_now, + ) + + async def async_will_remove_from_hass(self) -> None: + """Remove exprire triggers.""" + # Clean up expire triggers + if self._expiration_trigger: + _LOGGER.debug("Clean up expire after trigger for %s", self.entity_id) + self._expiration_trigger() + self._expiration_trigger = None + self._expired = False + await MqttEntity.async_will_remove_from_hass(self) + @staticmethod def config_schema(): """Return the config schema.""" diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index a13f0781dfb..917f046551d 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -36,6 +36,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_reload_with_config, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, @@ -44,7 +45,11 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message, async_fire_time_changed +from tests.common import ( + assert_setup_component, + async_fire_mqtt_message, + async_fire_time_changed, +) DEFAULT_CONFIG = { binary_sensor.DOMAIN: { @@ -868,3 +873,87 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = binary_sensor.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +async def test_cleanup_triggers_and_restoring_state( + hass, mqtt_mock, caplog, tmp_path, freezer +): + """Test cleanup old triggers at reloading and restoring the state.""" + domain = binary_sensor.DOMAIN + config1 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config1["name"] = "test1" + config1["expire_after"] = 30 + config1["state_topic"] = "test-topic1" + config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config2["name"] = "test2" + config2["expire_after"] = 5 + config2["state_topic"] = "test-topic2" + + freezer.move_to("2022-02-02 12:01:00+01:00") + + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + {binary_sensor.DOMAIN: [config1, config2]}, + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic1", "ON") + state = hass.states.get("binary_sensor.test1") + assert state.state == "on" + + async_fire_mqtt_message(hass, "test-topic2", "ON") + state = hass.states.get("binary_sensor.test2") + assert state.state == "on" + + freezer.move_to("2022-02-02 12:01:10+01:00") + + await help_test_reload_with_config( + hass, caplog, tmp_path, domain, [config1, config2] + ) + assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text + assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text + assert ( + "State recovered after reload for binary_sensor.test1, remaining time before expiring" + in caplog.text + ) + assert "State recovered after reload for binary_sensor.test2" not in caplog.text + + state = hass.states.get("binary_sensor.test1") + assert state.state == "on" + + state = hass.states.get("binary_sensor.test2") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "test-topic1", "OFF") + state = hass.states.get("binary_sensor.test1") + assert state.state == "off" + + async_fire_mqtt_message(hass, "test-topic2", "OFF") + state = hass.states.get("binary_sensor.test2") + assert state.state == "off" + + +async def test_skip_restoring_state_with_over_due_expire_trigger( + hass, mqtt_mock, caplog, freezer +): + """Test restoring a state with over due expire timer.""" + + freezer.move_to("2022-02-02 12:02:00+01:00") + domain = binary_sensor.DOMAIN + config3 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config3["name"] = "test3" + config3["expire_after"] = 10 + config3["state_topic"] = "test-topic3" + fake_state = ha.State( + "binary_sensor.test3", + "on", + {}, + last_changed=datetime.fromisoformat("2022-02-02 12:01:35+01:00"), + ) + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ), assert_setup_component(1, domain): + assert await async_setup_component(hass, domain, {domain: config3}) + await hass.async_block_till_done() + assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index ba2c9e3871a..593d08f4c87 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1525,6 +1525,25 @@ async def help_test_publishing_with_custom_encoding( mqtt_mock.async_publish.reset_mock() +async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): + """Test reloading with supplied config.""" + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({domain: config}) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await hass.services.async_call( + "mqtt", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert "" in caplog.text + + async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config): """Test reloading an MQTT platform.""" # Create and test an old config of 2 entities based on the config supplied @@ -1549,21 +1568,10 @@ async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config new_config_2["name"] = "test_new_2" new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" - new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({domain: [new_config_1, new_config_2, new_config_3]}) - new_yaml_config_file.write_text(new_yaml_config) - assert new_yaml_config_file.read_text() == new_yaml_config - with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): - await hass.services.async_call( - "mqtt", - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert "" in caplog.text + await help_test_reload_with_config( + hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] + ) assert len(hass.states.async_all(domain)) == 3 diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index a511938f0d1..ad43a7b2353 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -43,6 +43,7 @@ from .test_common import ( help_test_entity_disabled_by_default, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_reload_with_config, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, @@ -52,7 +53,11 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message, async_fire_time_changed +from tests.common import ( + assert_setup_component, + async_fire_mqtt_message, + async_fire_time_changed, +) DEFAULT_CONFIG = { sensor.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} @@ -935,6 +940,92 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +async def test_cleanup_triggers_and_restoring_state( + hass, mqtt_mock, caplog, tmp_path, freezer +): + """Test cleanup old triggers at reloading and restoring the state.""" + domain = sensor.DOMAIN + config1 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config1["name"] = "test1" + config1["expire_after"] = 30 + config1["state_topic"] = "test-topic1" + config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config2["name"] = "test2" + config2["expire_after"] = 5 + config2["state_topic"] = "test-topic2" + + freezer.move_to("2022-02-02 12:01:00+01:00") + + assert await async_setup_component( + hass, + domain, + {domain: [config1, config2]}, + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic1", "100") + state = hass.states.get("sensor.test1") + assert state.state == "100" + + async_fire_mqtt_message(hass, "test-topic2", "200") + state = hass.states.get("sensor.test2") + assert state.state == "200" + + freezer.move_to("2022-02-02 12:01:10+01:00") + + await help_test_reload_with_config( + hass, caplog, tmp_path, domain, [config1, config2] + ) + await hass.async_block_till_done() + + assert "Clean up expire after trigger for sensor.test1" in caplog.text + assert "Clean up expire after trigger for sensor.test2" not in caplog.text + assert ( + "State recovered after reload for sensor.test1, remaining time before expiring" + in caplog.text + ) + assert "State recovered after reload for sensor.test2" not in caplog.text + + state = hass.states.get("sensor.test1") + assert state.state == "100" + + state = hass.states.get("sensor.test2") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "test-topic1", "101") + state = hass.states.get("sensor.test1") + assert state.state == "101" + + async_fire_mqtt_message(hass, "test-topic2", "201") + state = hass.states.get("sensor.test2") + assert state.state == "201" + + +async def test_skip_restoring_state_with_over_due_expire_trigger( + hass, mqtt_mock, caplog, freezer +): + """Test restoring a state with over due expire timer.""" + + freezer.move_to("2022-02-02 12:02:00+01:00") + domain = sensor.DOMAIN + config3 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config3["name"] = "test3" + config3["expire_after"] = 10 + config3["state_topic"] = "test-topic3" + fake_state = ha.State( + "sensor.test3", + "300", + {}, + last_changed=datetime.fromisoformat("2022-02-02 12:01:35+01:00"), + ) + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ), assert_setup_component(1, domain): + assert await async_setup_component(hass, domain, {domain: config3}) + await hass.async_block_till_done() + assert "Skip state recovery after reload for sensor.test3" in caplog.text + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ From 95d4be375ccd9c5c341ec52c6350b530aea9d42e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Feb 2022 16:51:28 -0600 Subject: [PATCH 127/298] Handle brightness being None for senseme (#65372) --- homeassistant/components/senseme/light.py | 3 ++- tests/components/senseme/test_light.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/senseme/light.py b/homeassistant/components/senseme/light.py index 75d853c4001..3036dc1d04d 100644 --- a/homeassistant/components/senseme/light.py +++ b/homeassistant/components/senseme/light.py @@ -51,7 +51,8 @@ class HASensemeLight(SensemeEntity, LightEntity): def _async_update_attrs(self) -> None: """Update attrs from device.""" self._attr_is_on = self._device.light_on - self._attr_brightness = int(min(255, self._device.light_brightness * 16)) + if self._device.light_brightness is not None: + self._attr_brightness = int(min(255, self._device.light_brightness * 16)) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" diff --git a/tests/components/senseme/test_light.py b/tests/components/senseme/test_light.py index 21811452610..c585cfc31bf 100644 --- a/tests/components/senseme/test_light.py +++ b/tests/components/senseme/test_light.py @@ -74,6 +74,21 @@ async def test_fan_light(hass: HomeAssistant) -> None: assert device.light_on is True +async def test_fan_light_no_brightness(hass: HomeAssistant) -> None: + """Test a fan light without brightness.""" + device = _mock_device() + device.brightness = None + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_BRIGHTNESS] + + async def test_standalone_light(hass: HomeAssistant) -> None: """Test a standalone light.""" device = _mock_device() From 40a174cc70c687485ca942a0ee3e55a49d933fca Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Feb 2022 22:11:21 -0600 Subject: [PATCH 128/298] Detect battery-operated Sonos devices going offline (#65382) --- homeassistant/components/sonos/const.py | 1 + homeassistant/components/sonos/speaker.py | 34 ++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index bbeaceb08cd..abb0696360b 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -160,6 +160,7 @@ SONOS_SPEAKER_ACTIVITY = "sonos_speaker_activity" SONOS_SPEAKER_ADDED = "sonos_speaker_added" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_REBOOTED = "sonos_rebooted" +SONOS_VANISHED = "sonos_vanished" SOURCE_LINEIN = "Line-in" SOURCE_TV = "TV" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 5f06fe976ac..7777265a124 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -12,6 +12,7 @@ from typing import Any import urllib.parse import async_timeout +import defusedxml.ElementTree as ET from soco.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from soco.events_base import Event as SonosEvent, SubscriptionBase @@ -56,6 +57,7 @@ from .const import ( SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, SONOS_STATE_UPDATED, + SONOS_VANISHED, SOURCE_LINEIN, SOURCE_TV, SUBSCRIPTION_TIMEOUT, @@ -225,6 +227,7 @@ class SonosSpeaker: (SONOS_SPEAKER_ADDED, self.update_group_for_uid), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), (f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity), + (f"{SONOS_VANISHED}-{self.soco.uid}", self.async_vanished), ) for (signal, target) in dispatch_pairs: @@ -388,6 +391,8 @@ class SonosSpeaker: async def async_unsubscribe(self) -> None: """Cancel all subscriptions.""" + if not self._subscriptions: + return _LOGGER.debug("Unsubscribing from events for %s", self.zone_name) results = await asyncio.gather( *(subscription.unsubscribe() for subscription in self._subscriptions), @@ -572,6 +577,15 @@ class SonosSpeaker: self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) self.async_write_entity_states() + async def async_vanished(self, reason: str) -> None: + """Handle removal of speaker when marked as vanished.""" + if not self.available: + return + _LOGGER.debug( + "%s has vanished (%s), marking unavailable", self.zone_name, reason + ) + await self.async_offline() + async def async_rebooted(self, soco: SoCo) -> None: """Handle a detected speaker reboot.""" _LOGGER.warning( @@ -685,7 +699,25 @@ class SonosSpeaker: @callback def async_update_groups(self, event: SonosEvent) -> None: """Handle callback for topology change event.""" - if not hasattr(event, "zone_player_uui_ds_in_group"): + if xml := event.variables.get("zone_group_state"): + zgs = ET.fromstring(xml) + for vanished_device in zgs.find("VanishedDevices"): + if (reason := vanished_device.get("Reason")) != "sleeping": + _LOGGER.debug( + "Ignoring %s marked %s as vanished with reason: %s", + self.zone_name, + vanished_device.get("ZoneName"), + reason, + ) + continue + uid = vanished_device.get("UUID") + async_dispatcher_send( + self.hass, + f"{SONOS_VANISHED}-{uid}", + reason, + ) + + if "zone_player_uui_ds_in_group" not in event.variables: return self.event_stats.process(event) self.hass.async_create_task(self.create_update_groups_coro(event)) From fcd14e2830b5c914b3fee94c448c4c865adab131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 1 Feb 2022 22:44:06 +0100 Subject: [PATCH 129/298] Fix disconnect bug in Apple TV integration (#65385) --- homeassistant/components/apple_tv/__init__.py | 3 --- homeassistant/components/apple_tv/media_player.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index deb65c1f0a8..bd511d84eb5 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -179,7 +179,6 @@ class AppleTVManager: def _handle_disconnect(self): """Handle that the device disconnected and restart connect loop.""" if self.atv: - self.atv.listener = None self.atv.close() self.atv = None self._dispatch_send(SIGNAL_DISCONNECTED) @@ -196,8 +195,6 @@ class AppleTVManager: self._is_on = False try: if self.atv: - self.atv.push_updater.listener = None - self.atv.push_updater.stop() self.atv.close() self.atv = None if self._task: diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 8abab0e0225..cf0afaa9b81 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -168,9 +168,6 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): @callback def async_device_disconnected(self): """Handle when connection was lost to device.""" - self.atv.push_updater.stop() - self.atv.push_updater.listener = None - self.atv.power.listener = None self._attr_supported_features = SUPPORT_APPLE_TV @property From 91023cf1327c3377f367555bd967336f724f9e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 1 Feb 2022 22:30:28 +0100 Subject: [PATCH 130/298] Sort Apple TV app list by name (#65386) --- homeassistant/components/apple_tv/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index cf0afaa9b81..1f8cabb1d14 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -162,7 +162,10 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): except exceptions.ProtocolError: _LOGGER.exception("Failed to update app list") else: - self._app_list = {app.name: app.identifier for app in apps} + self._app_list = { + app.name: app.identifier + for app in sorted(apps, key=lambda app: app.name.lower()) + } self.async_write_ha_state() @callback From 2b0e8287364fbab0551bd284e6411d675de75709 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Feb 2022 22:10:53 -0600 Subject: [PATCH 131/298] Fix Sonos diagnostics with offline device (#65393) Co-authored-by: J. Nick Koston --- homeassistant/components/sonos/diagnostics.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 707449e4002..421a0f7ed6a 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -91,9 +91,13 @@ async def async_generate_media_info( payload[attrib] = getattr(speaker.media, attrib) def poll_current_track_info(): - return speaker.soco.avTransport.GetPositionInfo( - [("InstanceID", 0), ("Channel", "Master")] - ) + try: + return speaker.soco.avTransport.GetPositionInfo( + [("InstanceID", 0), ("Channel", "Master")], + timeout=3, + ) + except OSError as ex: + return f"Error retrieving: {ex}" payload["current_track_poll"] = await hass.async_add_executor_job( poll_current_track_info From 690764ec840507f34e54b59ea638226aeb305cfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Feb 2022 19:19:24 -0600 Subject: [PATCH 132/298] Bump lutron_caseta to 0.13.1 to fix setup when no button devices are present (#65400) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index b6d3eb51f7a..fc29aa8ced7 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.13.0"], + "requirements": ["pylutron-caseta==0.13.1"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 66977136936..daf3f45a671 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1648,7 +1648,7 @@ pylitejet==0.3.0 pylitterbot==2021.12.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.13.0 +pylutron-caseta==0.13.1 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b48abc36b8..450f9a356c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1032,7 +1032,7 @@ pylitejet==0.3.0 pylitterbot==2021.12.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.13.0 +pylutron-caseta==0.13.1 # homeassistant.components.mailgun pymailgunner==1.4 From 1809489421cc41b914cdaada6e1317f9a6529753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Feb 2022 09:16:29 -0600 Subject: [PATCH 133/298] Ensure unifiprotect discovery can be ignored (#65406) --- .../components/unifiprotect/config_flow.py | 6 +++++- .../unifiprotect/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 720a46b4659..0890a962e5b 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -90,7 +90,11 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(mac) source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] - for entry in self._async_current_entries(include_ignore=False): + for entry in self._async_current_entries(): + if entry.source == config_entries.SOURCE_IGNORE: + if entry.unique_id == mac: + return self.async_abort(reason="already_configured") + continue entry_host = entry.data[CONF_HOST] entry_has_direct_connect = _host_is_direct_connect(entry_host) if entry.unique_id == mac: diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index fc5b2b32873..a1609984be3 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -723,3 +723,24 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_discovery_can_be_ignored(hass: HomeAssistant, mock_nvr: NVR) -> None: + """Test a discovery can be ignored.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={}, + unique_id=DEVICE_MAC_ADDRESS.upper().replace(":", ""), + source=config_entries.SOURCE_IGNORE, + ) + mock_config.add_to_hass(hass) + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 51c6cac74dffd3e6940e9444d953b6c2bde7f3c5 Mon Sep 17 00:00:00 2001 From: Josh Shoemaker Date: Wed, 2 Feb 2022 03:29:05 -0500 Subject: [PATCH 134/298] Bump aladdin_connect to 0.4 to fix integration for some users due to API changes (#65407) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index b2cc5f6d32c..6ab5c97d007 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["aladdin_connect==0.3"], + "requirements": ["aladdin_connect==0.4"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index daf3f45a671..6a87fea46cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -293,7 +293,7 @@ airthings_cloud==0.1.0 airtouch4pyapi==1.0.5 # homeassistant.components.aladdin_connect -aladdin_connect==0.3 +aladdin_connect==0.4 # homeassistant.components.alpha_vantage alpha_vantage==2.3.1 From 5190282b4d514bdc40b1820a567c55d427133adb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Feb 2022 17:58:14 +0100 Subject: [PATCH 135/298] Don't warn on time.sleep injected by the debugger (#65420) --- homeassistant/util/async_.py | 17 +++++++++++--- tests/util/test_async.py | 45 +++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 229a8fef366..8f9526b6800 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -88,7 +88,7 @@ def run_callback_threadsafe( return future -def check_loop(strict: bool = True) -> None: +def check_loop(func: Callable, strict: bool = True) -> None: """Warn if called inside the event loop. Raise if `strict` is True.""" try: get_running_loop() @@ -101,7 +101,18 @@ def check_loop(strict: bool = True) -> None: found_frame = None - for frame in reversed(extract_stack()): + stack = extract_stack() + + if ( + func.__name__ == "sleep" + and len(stack) >= 3 + and stack[-3].filename.endswith("pydevd.py") + ): + # Don't report `time.sleep` injected by the debugger (pydevd.py) + # stack[-1] is us, stack[-2] is protected_loop_func, stack[-3] is the offender + return + + for frame in reversed(stack): for path in ("custom_components/", "homeassistant/components/"): try: index = frame.filename.index(path) @@ -152,7 +163,7 @@ def protect_loop(func: Callable, strict: bool = True) -> Callable: @functools.wraps(func) def protected_loop_func(*args, **kwargs): # type: ignore - check_loop(strict=strict) + check_loop(func, strict=strict) return func(*args, **kwargs) return protected_loop_func diff --git a/tests/util/test_async.py b/tests/util/test_async.py index d272da8fe96..f02d3c03b4b 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest +from homeassistant import block_async_io from homeassistant.util import async_ as hasync @@ -70,10 +71,14 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): assert len(loop.call_soon_threadsafe.mock_calls) == 2 +def banned_function(): + """Mock banned function.""" + + async def test_check_loop_async(): """Test check_loop detects when called from event loop without integration context.""" with pytest.raises(RuntimeError): - hasync.check_loop() + hasync.check_loop(banned_function) async def test_check_loop_async_integration(caplog): @@ -98,7 +103,7 @@ async def test_check_loop_async_integration(caplog): ), ], ): - hasync.check_loop() + hasync.check_loop(banned_function) assert ( "Detected blocking call inside the event loop. This is causing stability issues. " "Please report issue for hue doing blocking calls at " @@ -129,7 +134,7 @@ async def test_check_loop_async_integration_non_strict(caplog): ), ], ): - hasync.check_loop(strict=False) + hasync.check_loop(banned_function, strict=False) assert ( "Detected blocking call inside the event loop. This is causing stability issues. " "Please report issue for hue doing blocking calls at " @@ -160,7 +165,7 @@ async def test_check_loop_async_custom(caplog): ), ], ): - hasync.check_loop() + hasync.check_loop(banned_function) assert ( "Detected blocking call inside the event loop. This is causing stability issues. " "Please report issue to the custom component author for hue doing blocking calls " @@ -170,7 +175,7 @@ async def test_check_loop_async_custom(caplog): def test_check_loop_sync(caplog): """Test check_loop does nothing when called from thread.""" - hasync.check_loop() + hasync.check_loop(banned_function) assert "Detected blocking call inside the event loop" not in caplog.text @@ -179,10 +184,38 @@ def test_protect_loop_sync(): func = Mock() with patch("homeassistant.util.async_.check_loop") as mock_check_loop: hasync.protect_loop(func)(1, test=2) - mock_check_loop.assert_called_once_with(strict=True) + mock_check_loop.assert_called_once_with(func, strict=True) func.assert_called_once_with(1, test=2) +async def test_protect_loop_debugger_sleep(caplog): + """Test time.sleep injected by the debugger is not reported.""" + block_async_io.enable() + + with patch( + "homeassistant.util.async_.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/util/async.py", + lineno="123", + line="protected_loop_func", + ), + Mock( + filename="/home/paulus/homeassistant/util/async.py", + lineno="123", + line="check_loop()", + ), + ], + ): + time.sleep(0) + assert "Detected blocking call inside the event loop" not in caplog.text + + async def test_gather_with_concurrency(): """Test gather_with_concurrency limits the number of running tasks.""" From dacf5957d2813fc2f7e975010020069717a91b74 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 2 Feb 2022 14:38:19 +0100 Subject: [PATCH 136/298] Bump velbus-aio to 2022.2.1 (#65422) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index f52ba0fd99d..3ffb29632e0 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.11.7"], + "requirements": ["velbus-aio==2022.2.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 6a87fea46cf..e5799850c98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2423,7 +2423,7 @@ vallox-websocket-api==2.9.0 vehicle==0.3.1 # homeassistant.components.velbus -velbus-aio==2021.11.7 +velbus-aio==2022.2.1 # homeassistant.components.venstar venstarcolortouch==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 450f9a356c5..0f6dc4beeb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1481,7 +1481,7 @@ vallox-websocket-api==2.9.0 vehicle==0.3.1 # homeassistant.components.velbus -velbus-aio==2021.11.7 +velbus-aio==2022.2.1 # homeassistant.components.venstar venstarcolortouch==0.15 From ec2e45044294ecb74315ccfe6f4ffecd8409ac6a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Feb 2022 18:08:48 +0100 Subject: [PATCH 137/298] Stringify MQTT payload in mqtt/debug/info WS response (#65429) --- homeassistant/components/mqtt/debug_info.py | 2 +- tests/components/mqtt/test_common.py | 2 +- tests/components/mqtt/test_init.py | 64 ++++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index e462d76fa31..3e32d301b70 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -139,7 +139,7 @@ async def info_for_device(hass, device_id): "topic": topic, "messages": [ { - "payload": msg.payload, + "payload": str(msg.payload), "qos": msg.qos, "retain": msg.retain, "time": msg.timestamp, diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 593d08f4c87..78c37b1105a 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1222,7 +1222,7 @@ async def help_test_entity_debug_info_message( "topic": topic, "messages": [ { - "payload": payload, + "payload": str(payload), "qos": 0, "retain": False, "time": start_dt, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a71c332b70b..9101b895218 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3,7 +3,7 @@ import asyncio from datetime import datetime, timedelta import json import ssl -from unittest.mock import AsyncMock, MagicMock, call, mock_open, patch +from unittest.mock import ANY, AsyncMock, MagicMock, call, mock_open, patch import pytest import voluptuous as vol @@ -1540,6 +1540,68 @@ async def test_mqtt_ws_get_device_debug_info( assert response["result"] == expected_result +async def test_mqtt_ws_get_device_debug_info_binary( + hass, device_reg, hass_ws_client, mqtt_mock +): + """Test MQTT websocket device debug info.""" + config = { + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "topic": "foobar/image", + "unique_id": "unique", + } + data = json.dumps(config) + + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + # Verify device entry is created + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + assert device_entry is not None + + small_png = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x04\x08\x06" + b"\x00\x00\x00\xa9\xf1\x9e~\x00\x00\x00\x13IDATx\xdac\xfc\xcf\xc0P\xcf\x80\x04" + b"\x18I\x17\x00\x00\xf2\xae\x05\xfdR\x01\xc2\xde\x00\x00\x00\x00IEND\xaeB`\x82" + ) + async_fire_mqtt_message(hass, "foobar/image", small_png) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "mqtt/device/debug_info", "device_id": device_entry.id} + ) + response = await client.receive_json() + assert response["success"] + expected_result = { + "entities": [ + { + "entity_id": "camera.mqtt_camera", + "subscriptions": [ + { + "topic": "foobar/image", + "messages": [ + { + "payload": str(small_png), + "qos": 0, + "retain": False, + "time": ANY, + "topic": "foobar/image", + } + ], + } + ], + "discovery_data": { + "payload": config, + "topic": "homeassistant/camera/bla/config", + }, + } + ], + "triggers": [], + } + assert response["result"] == expected_result + + async def test_debug_info_multiple_devices(hass, mqtt_mock): """Test we get correct debug_info when multiple devices are present.""" devices = [ From 8851af7dbabb077eb0cf4e71bd1873a36a6e6656 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Feb 2022 16:08:16 +0100 Subject: [PATCH 138/298] Update frontend to 20220202.0 (#65432) --- 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 49e49ac2efa..4f1e6eff032 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220201.0" + "home-assistant-frontend==20220202.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 30086a676e4..438fa0eba8d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.6.3 hass-nabucasa==0.52.0 -home-assistant-frontend==20220201.0 +home-assistant-frontend==20220202.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index e5799850c98..213388b0f3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220201.0 +home-assistant-frontend==20220202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f6dc4beeb5..7aba9ba5fa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220201.0 +home-assistant-frontend==20220202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 580573fcb3bfe772060dba2552d37300dff27cee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Feb 2022 18:12:26 +0100 Subject: [PATCH 139/298] Bumped version to 2022.2.0 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index db26c8170b3..c6403514ac9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 18765d7e8e0..970a736b68f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.0b6 +version = 2022.2.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 1155d229f3e030830a313fd106de30dee0978632 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 3 Feb 2022 02:07:12 +0100 Subject: [PATCH 140/298] Get wind speed unit from AccuWeather data (#65425) --- homeassistant/components/accuweather/weather.py | 3 +++ tests/components/accuweather/test_weather.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index c97cc44aea3..00726f6db38 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -62,6 +62,9 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): """Initialize.""" super().__init__(coordinator) self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL + self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][ + self._unit_system + ]["Unit"] self._attr_name = name self._attr_unique_id = coordinator.location_key self._attr_temperature_unit = ( diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 6c1bc76e9b1..02ace5d3f1d 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -46,7 +46,7 @@ async def test_weather_without_forecast(hass): assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.03 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION entry = registry.async_get("weather.home") @@ -68,7 +68,7 @@ async def test_weather_with_forecast(hass): assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.03 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" @@ -78,7 +78,7 @@ async def test_weather_with_forecast(hass): assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 15.4 assert forecast.get(ATTR_FORECAST_TIME) == "2020-07-26T05:00:00+00:00" assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 166 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 13.0 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 3.61 entry = registry.async_get("weather.home") assert entry From 1ae2bfcc89396244d241b0a91a67939e2f78bd49 Mon Sep 17 00:00:00 2001 From: Colin Robbins Date: Wed, 2 Feb 2022 19:44:16 +0000 Subject: [PATCH 141/298] Fix Shodan sensor (#65443) --- homeassistant/components/shodan/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index 6fc73f40096..bdef681fdd2 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -67,7 +67,7 @@ class ShodanSensor(SensorEntity): def update(self) -> None: """Get the latest data and updates the states.""" data = self.data.update() - self._attr_native_value = data.details["total"] + self._attr_native_value = data["total"] class ShodanData: From f7ec373aab2c08d5f7d09c4a5b14be8bb1e982a9 Mon Sep 17 00:00:00 2001 From: mk-maddin <46523240+mk-maddin@users.noreply.github.com> Date: Thu, 3 Feb 2022 17:18:58 +0100 Subject: [PATCH 142/298] Fix script / automation repeat with count 0 fails (#65448) Co-authored-by: Paulus Schoutsen Co-authored-by: Erik Montnemery --- homeassistant/helpers/script.py | 2 +- tests/helpers/test_script.py | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 8e54e294f4b..5a80691fa46 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -742,7 +742,7 @@ class _ScriptRun: if saved_repeat_vars: self._variables["repeat"] = saved_repeat_vars else: - del self._variables["repeat"] + self._variables.pop("repeat", None) # Not set if count = 0 async def _async_choose_step(self) -> None: """Choose a sequence.""" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 2ee4213a688..5bb4833a796 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1742,6 +1742,44 @@ async def test_repeat_count(hass, caplog, count): ) +async def test_repeat_count_0(hass, caplog): + """Test repeat action w/ count option.""" + event = "test_event" + events = async_capture_events(hass, event) + count = 0 + + alias = "condition step" + sequence = cv.SCRIPT_SCHEMA( + { + "alias": alias, + "repeat": { + "count": count, + "sequence": { + "event": event, + "event_data_template": { + "first": "{{ repeat.first }}", + "index": "{{ repeat.index }}", + "last": "{{ repeat.last }}", + }, + }, + }, + } + ) + + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert len(events) == count + assert caplog.text.count(f"Repeating {alias}") == count + assert_action_trace( + { + "0": [{}], + } + ) + + @pytest.mark.parametrize("condition", ["while", "until"]) async def test_repeat_condition_warning(hass, caplog, condition): """Test warning on repeat conditions.""" From a8b29c4be9af3215c0341036c058a78afa688965 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 2 Feb 2022 18:06:24 -0700 Subject: [PATCH 143/298] Fix `unknown alarm websocket event` error for restored SimpliSafe connections (#65457) --- .../components/simplisafe/__init__.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index a8cdb853749..a133ec6c2dc 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -820,17 +820,6 @@ class SimpliSafeEntity(CoordinatorEntity): ): return - if event.event_type in (EVENT_CONNECTION_LOST, EVENT_POWER_OUTAGE): - self._online = False - elif event.event_type in (EVENT_CONNECTION_RESTORED, EVENT_POWER_RESTORED): - self._online = True - - # It's uncertain whether SimpliSafe events will still propagate down the - # websocket when the base station is offline. Just in case, we guard against - # further action until connection is restored: - if not self._online: - return - sensor_type: str | None if event.sensor_type: sensor_type = event.sensor_type.name @@ -846,6 +835,19 @@ class SimpliSafeEntity(CoordinatorEntity): } ) + # It's unknown whether these events reach the base station (since the connection + # is lost); we include this for completeness and coverage: + if event.event_type in (EVENT_CONNECTION_LOST, EVENT_POWER_OUTAGE): + self._online = False + return + + # If the base station comes back online, set entities to available, but don't + # instruct the entities to update their state (since there won't be anything new + # until the next websocket event or REST API update: + if event.event_type in (EVENT_CONNECTION_RESTORED, EVENT_POWER_RESTORED): + self._online = True + return + self.async_update_from_websocket_event(event) self.async_write_ha_state() From d195e8a1b4e0cd6ae7545faed64201f8b1fd524f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 2 Feb 2022 18:05:40 -0700 Subject: [PATCH 144/298] Catch correct error during OpenUV startup (#65459) --- homeassistant/components/openuv/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 2f186af2ffe..774cd05fd9f 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await openuv.async_update() - except OpenUvError as err: + except HomeAssistantError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err From 8d33964e4dbf1401b941e22b4054dd5ffa65692c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Feb 2022 06:22:34 -0600 Subject: [PATCH 145/298] Fix vanished checks on old Sonos firmware (#65477) --- homeassistant/components/sonos/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 7777265a124..b5ae20e1123 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -701,7 +701,7 @@ class SonosSpeaker: """Handle callback for topology change event.""" if xml := event.variables.get("zone_group_state"): zgs = ET.fromstring(xml) - for vanished_device in zgs.find("VanishedDevices"): + for vanished_device in zgs.find("VanishedDevices") or []: if (reason := vanished_device.get("Reason")) != "sleeping": _LOGGER.debug( "Ignoring %s marked %s as vanished with reason: %s", From 6550d04313b835488579681bd51612d6b322e984 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 3 Feb 2022 06:31:15 -0700 Subject: [PATCH 146/298] Allow Flu Near You to re-attempt startup on error (#65481) --- homeassistant/components/flunearyou/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index bb07e9ccc73..9a2ef2d4465 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=DEFAULT_UPDATE_INTERVAL, update_method=partial(async_update, api_category), ) - data_init_tasks.append(coordinator.async_refresh()) + data_init_tasks.append(coordinator.async_config_entry_first_refresh()) await asyncio.gather(*data_init_tasks) hass.data.setdefault(DOMAIN, {}) From ec0b0e41a1a3096d5f147de7c84a80221d4eebcb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 3 Feb 2022 06:26:30 -0700 Subject: [PATCH 147/298] Bump pytile to 2022.02.0 (#65482) --- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 1b30e0483f7..b8e1e14ac8b 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==2022.01.0"], + "requirements": ["pytile==2022.02.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 213388b0f3c..eff8c5736ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1999,7 +1999,7 @@ python_opendata_transport==0.3.0 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==2022.01.0 +pytile==2022.02.0 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7aba9ba5fa7..18f97f5ba90 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1230,7 +1230,7 @@ python-twitch-client==0.6.0 python_awair==0.2.1 # homeassistant.components.tile -pytile==2022.01.0 +pytile==2022.02.0 # homeassistant.components.traccar pytraccar==0.10.0 From faa8ac692e98c389b3cd96d8a4ff41a3e8b2d926 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 3 Feb 2022 14:29:12 +0100 Subject: [PATCH 148/298] Fix SIA availability (#65509) --- homeassistant/components/sia/const.py | 1 + homeassistant/components/sia/sia_entity_base.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index 82ef6ad9429..537c106fefa 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -38,3 +38,4 @@ KEY_MOISTURE: Final = "moisture" KEY_POWER: Final = "power" PREVIOUS_STATE: Final = "previous_state" +AVAILABILITY_EVENT_CODE: Final = "RP" diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index fee3c0b2262..311728ad578 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -14,7 +14,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType -from .const import DOMAIN, SIA_EVENT, SIA_HUB_ZONE +from .const import AVAILABILITY_EVENT_CODE, DOMAIN, SIA_EVENT, SIA_HUB_ZONE from .utils import get_attr_from_sia_event, get_unavailability_interval _LOGGER = logging.getLogger(__name__) @@ -105,7 +105,7 @@ class SIABaseEntity(RestoreEntity): return self._attr_extra_state_attributes.update(get_attr_from_sia_event(sia_event)) state_changed = self.update_state(sia_event) - if state_changed: + if state_changed or sia_event.code == AVAILABILITY_EVENT_CODE: self.async_reset_availability_cb() self.async_write_ha_state() From 689133976a814ba59fb18933521a98824c54f651 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Feb 2022 14:00:03 +0100 Subject: [PATCH 149/298] Fix missing windspeed in Tuya climate (#65511) --- homeassistant/components/tuya/climate.py | 4 +++- homeassistant/components/tuya/const.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index cbb778e34b1..a97a27a7453 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -223,7 +223,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): # Determine fan modes if enum_type := self.find_dpcode( - DPCode.FAN_SPEED_ENUM, dptype=DPType.ENUM, prefer_function=True + (DPCode.FAN_SPEED_ENUM, DPCode.WINDSPEED), + dptype=DPType.ENUM, + prefer_function=True, ): self._attr_supported_features |= SUPPORT_FAN_MODE self._attr_fan_modes = enum_type.range diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 5093745a0bf..6d6a2aa2937 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -360,6 +360,7 @@ class DPCode(StrEnum): WATER_SET = "water_set" # Water level WATERSENSOR_STATE = "watersensor_state" WET = "wet" # Humidification + WINDSPEED = "windspeed" WIRELESS_BATTERYLOCK = "wireless_batterylock" WIRELESS_ELECTRICITY = "wireless_electricity" WORK_MODE = "work_mode" # Working mode From 931c27f4527c3cacb833c02c32dd938d7d702dfe Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Feb 2022 16:46:36 +0100 Subject: [PATCH 150/298] Return current state if template throws (#65534) --- homeassistant/components/mqtt/sensor.py | 2 +- tests/components/mqtt/test_sensor.py | 32 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 59f124155d3..fa949009a0d 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -236,7 +236,7 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): self.hass, self._value_is_expired, expiration_at ) - payload = self._template(msg.payload) + payload = self._template(msg.payload, default=self._state) if payload is not None and self.device_class in ( SensorDeviceClass.DATE, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index ad43a7b2353..c758b670b3d 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -268,6 +268,38 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): assert state.state == "100" +async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( + hass, mqtt_mock +): + """Test the setting of the value via MQTT with fall back to current state.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message( + hass, "test-topic", '{ "val": "valcontent", "par": "parcontent" }' + ) + state = hass.states.get("sensor.test") + + assert state.state == "valcontent-parcontent" + + async_fire_mqtt_message(hass, "test-topic", '{ "par": "invalidcontent" }') + state = hass.states.get("sensor.test") + + assert state.state == "valcontent-parcontent" + + async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplog): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( From afbc55b1817f7df6f24d62aeaf5345f45cfb4a7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Feb 2022 12:14:36 -0600 Subject: [PATCH 151/298] Do not update unifiprotect host from discovery if its not an ip (#65548) --- .../components/unifiprotect/config_flow.py | 7 ++++- .../unifiprotect/test_config_flow.py | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 0890a962e5b..39e255bb715 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_integration +from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, @@ -105,7 +106,11 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): and entry_host != direct_connect_domain ): new_host = direct_connect_domain - elif not entry_has_direct_connect and entry_host != source_ip: + elif ( + not entry_has_direct_connect + and is_ip_address(entry_host) + and entry_host != source_ip + ): new_host = source_ip if new_host: self.hass.config_entries.async_update_entry( diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index a1609984be3..68d90ff82eb 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -418,6 +418,37 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin assert mock_config.data[CONF_HOST] == "127.0.0.1" +async def test_discovered_host_not_updated_if_existing_is_a_hostname( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test we only update the host if its an ip address from discovery.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "a.hostname", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": True, + }, + unique_id=DEVICE_MAC_ADDRESS.upper().replace(":", ""), + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_config.data[CONF_HOST] == "a.hostname" + + async def test_discovered_by_unifi_discovery( hass: HomeAssistant, mock_nvr: NVR ) -> None: From 2bb65ecf384ba0c852ab2a320b9bf255ed2d9a71 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 3 Feb 2022 21:32:36 +0100 Subject: [PATCH 152/298] Fix data update when guest client disappears in Fritz!Tools (#65564) Co-authored-by: Simone Chemelli --- homeassistant/components/fritz/common.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index dede4b82c02..fa4096cb4ff 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -220,8 +220,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): """Update FritzboxTools data.""" try: await self.async_scan_devices() - except (FritzSecurityError, FritzConnectionException) as ex: - raise update_coordinator.UpdateFailed from ex + except FRITZ_EXCEPTIONS as ex: + raise update_coordinator.UpdateFailed(ex) from ex @property def unique_id(self) -> str: @@ -294,11 +294,19 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): def _get_wan_access(self, ip_address: str) -> bool | None: """Get WAN access rule for given IP address.""" - return not self.connection.call_action( - "X_AVM-DE_HostFilter:1", - "GetWANAccessByIP", - NewIPv4Address=ip_address, - ).get("NewDisallow") + try: + return not self.connection.call_action( + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=ip_address, + ).get("NewDisallow") + except FRITZ_EXCEPTIONS as ex: + _LOGGER.debug( + "could not get WAN access rule for client device with IP '%s', error: %s", + ip_address, + ex, + ) + return None async def async_scan_devices(self, now: datetime | None = None) -> None: """Wrap up FritzboxTools class scan.""" From e32a54eecc07f6043ff285119bbf620fe38a9cb8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Feb 2022 20:31:22 +0100 Subject: [PATCH 153/298] Add missing Tuya vacuum states (#65567) --- homeassistant/components/tuya/vacuum.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 832128a8b3e..70e1691af92 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -37,6 +37,7 @@ TUYA_MODE_RETURN_HOME = "chargego" TUYA_STATUS_TO_HA = { "charge_done": STATE_DOCKED, "chargecompleted": STATE_DOCKED, + "chargego": STATE_DOCKED, "charging": STATE_DOCKED, "cleaning": STATE_CLEANING, "docking": STATE_RETURNING, @@ -48,11 +49,14 @@ TUYA_STATUS_TO_HA = { "pick_zone_clean": STATE_CLEANING, "pos_arrived": STATE_CLEANING, "pos_unarrive": STATE_CLEANING, + "random": STATE_CLEANING, "sleep": STATE_IDLE, "smart_clean": STATE_CLEANING, + "smart": STATE_CLEANING, "spot_clean": STATE_CLEANING, "standby": STATE_IDLE, "wall_clean": STATE_CLEANING, + "wall_follow": STATE_CLEANING, "zone_clean": STATE_CLEANING, } From 92f4f99d41932dac17a19e44350605be29d1c1f6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 4 Feb 2022 00:05:56 +0100 Subject: [PATCH 154/298] Add back resolvers config flow dnsip (#65570) --- homeassistant/components/dnsip/config_flow.py | 17 ++++++- homeassistant/components/dnsip/strings.json | 4 +- .../components/dnsip/translations/en.json | 4 +- tests/components/dnsip/test_config_flow.py | 44 +++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index bedcc5f821c..2db0034b697 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -33,6 +33,13 @@ DATA_SCHEMA = vol.Schema( vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, } ) +DATA_SCHEMA_ADV = vol.Schema( + { + vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, + vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, + vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, + } +) async def async_validate_hostname( @@ -94,8 +101,8 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): hostname = user_input[CONF_HOSTNAME] name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname - resolver = DEFAULT_RESOLVER - resolver_ipv6 = DEFAULT_RESOLVER_IPV6 + resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER) + resolver_ipv6 = user_input.get(CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6) validate = await async_validate_hostname(hostname, resolver, resolver_ipv6) @@ -119,6 +126,12 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) + if self.show_advanced_options is True: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA_ADV, + errors=errors, + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index 06672e6fb68..cd95c9db27f 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "hostname": "The hostname for which to perform the DNS query" + "hostname": "The hostname for which to perform the DNS query", + "resolver": "Resolver for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup" } } }, diff --git a/homeassistant/components/dnsip/translations/en.json b/homeassistant/components/dnsip/translations/en.json index 7b2e2f9e6c7..2c773375860 100644 --- a/homeassistant/components/dnsip/translations/en.json +++ b/homeassistant/components/dnsip/translations/en.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "The hostname for which to perform the DNS query" + "hostname": "The hostname for which to perform the DNS query", + "resolver": "Resolver for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup" } } } diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 59dcb81aa94..f4684eb1cc4 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -7,6 +7,7 @@ from aiodns.error import DNSError import pytest from homeassistant import config_entries +from homeassistant.components.dnsip.config_flow import DATA_SCHEMA, DATA_SCHEMA_ADV from homeassistant.components.dnsip.const import ( CONF_HOSTNAME, CONF_IPV4, @@ -47,6 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" + assert result["data_schema"] == DATA_SCHEMA assert result["errors"] == {} with patch( @@ -79,6 +81,48 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_adv(hass: HomeAssistant) -> None: + """Test we get the form with advanced options on.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, + ) + + assert result["data_schema"] == DATA_SCHEMA_ADV + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ), patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTNAME: "home-assistant.io", + CONF_RESOLVER: "8.8.8.8", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "home-assistant.io" + assert result2["data"] == { + "hostname": "home-assistant.io", + "name": "home-assistant.io", + "ipv4": True, + "ipv6": True, + } + assert result2["options"] == { + "resolver": "8.8.8.8", + "resolver_ipv6": "2620:0:ccc::2", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_error(hass: HomeAssistant) -> None: """Test validate url fails.""" result = await hass.config_entries.flow.async_init( From b9a37e2c3ed5a36d2a717ec69f1dd5c1b39438a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Feb 2022 21:46:05 +0100 Subject: [PATCH 155/298] Guard against empty Tuya data types (#65571) --- homeassistant/components/tuya/base.py | 32 ++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index ac9a9c83be2..15e57f223e9 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -72,9 +72,11 @@ class IntegerTypeData: return remap_value(value, from_min, from_max, self.min, self.max, reverse) @classmethod - def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData: + def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData | None: """Load JSON string and return a IntegerTypeData object.""" - parsed = json.loads(data) + if not (parsed := json.loads(data)): + return None + return cls( dpcode, min=int(parsed["min"]), @@ -94,9 +96,11 @@ class EnumTypeData: range: list[str] @classmethod - def from_json(cls, dpcode: DPCode, data: str) -> EnumTypeData: + def from_json(cls, dpcode: DPCode, data: str) -> EnumTypeData | None: """Load JSON string and return a EnumTypeData object.""" - return cls(dpcode, **json.loads(data)) + if not (parsed := json.loads(data)): + return None + return cls(dpcode, **parsed) @dataclass @@ -222,17 +226,25 @@ class TuyaEntity(Entity): dptype == DPType.ENUM and getattr(self.device, key)[dpcode].type == DPType.ENUM ): - return EnumTypeData.from_json( - dpcode, getattr(self.device, key)[dpcode].values - ) + if not ( + enum_type := EnumTypeData.from_json( + dpcode, getattr(self.device, key)[dpcode].values + ) + ): + continue + return enum_type if ( dptype == DPType.INTEGER and getattr(self.device, key)[dpcode].type == DPType.INTEGER ): - return IntegerTypeData.from_json( - dpcode, getattr(self.device, key)[dpcode].values - ) + if not ( + integer_type := IntegerTypeData.from_json( + dpcode, getattr(self.device, key)[dpcode].values + ) + ): + continue + return integer_type if dptype not in (DPType.ENUM, DPType.INTEGER): return dpcode From 292893583883a5989a94f86b5b3a0d8fe87b1652 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Feb 2022 21:32:01 +0100 Subject: [PATCH 156/298] Update frontend to 20220203.0 (#65572) --- 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 4f1e6eff032..8a9b13648e9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220202.0" + "home-assistant-frontend==20220203.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 438fa0eba8d..3f0b9516ead 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.6.3 hass-nabucasa==0.52.0 -home-assistant-frontend==20220202.0 +home-assistant-frontend==20220203.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index eff8c5736ac..cc02fd71fbc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220202.0 +home-assistant-frontend==20220203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18f97f5ba90..c000ec60827 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220202.0 +home-assistant-frontend==20220203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3a1a12b13ee69979701270e8f9256a5de525069e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 3 Feb 2022 22:36:36 +0100 Subject: [PATCH 157/298] Extend diagnostics data in Fritz!Tools (#65573) --- homeassistant/components/fritz/common.py | 14 ++++++++++++++ homeassistant/components/fritz/diagnostics.py | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index fa4096cb4ff..2cd6616f134 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -574,6 +574,13 @@ class AvmWrapper(FritzBoxTools): partial(self.get_wan_dsl_interface_config) ) + async def async_get_wan_link_properties(self) -> dict[str, Any]: + """Call WANCommonInterfaceConfig service.""" + + return await self.hass.async_add_executor_job( + partial(self.get_wan_link_properties) + ) + async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" @@ -676,6 +683,13 @@ class AvmWrapper(FritzBoxTools): return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo") + def get_wan_link_properties(self) -> dict[str, Any]: + """Call WANCommonInterfaceConfig service.""" + + return self._service_call_action( + "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" + ) + def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]: """Call SetEnable action from WLANConfiguration service.""" diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index 4305ee4d7cb..f35eca6b914 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -29,6 +29,19 @@ async def async_get_config_entry_diagnostics( "mesh_role": avm_wrapper.mesh_role, "last_update success": avm_wrapper.last_update_success, "last_exception": avm_wrapper.last_exception, + "discovered_services": list(avm_wrapper.connection.services), + "client_devices": [ + { + "connected_to": device.connected_to, + "connection_type": device.connection_type, + "hostname": device.hostname, + "is_connected": device.is_connected, + "last_activity": device.last_activity, + "wan_access": device.wan_access, + } + for _, device in avm_wrapper.devices.items() + ], + "wan_link_properties": await avm_wrapper.async_get_wan_link_properties(), }, } From c8827e00b34363623601411bc743768d370f0290 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Feb 2022 22:37:10 +0100 Subject: [PATCH 158/298] Update pvo to 0.2.1 (#65584) --- homeassistant/components/pvoutput/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 378ad399ffe..042c6b9aa99 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/pvoutput", "config_flow": true, "codeowners": ["@fabaff", "@frenck"], - "requirements": ["pvo==0.2.0"], + "requirements": ["pvo==0.2.1"], "iot_class": "cloud_polling", "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index cc02fd71fbc..42800f50eec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1310,7 +1310,7 @@ pushbullet.py==0.11.0 pushover_complete==1.1.1 # homeassistant.components.pvoutput -pvo==0.2.0 +pvo==0.2.1 # homeassistant.components.rpi_gpio_pwm pwmled==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c000ec60827..83fed2a14e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -814,7 +814,7 @@ pure-python-adb[async]==0.3.0.dev0 pushbullet.py==0.11.0 # homeassistant.components.pvoutput -pvo==0.2.0 +pvo==0.2.1 # homeassistant.components.canary py-canary==0.5.1 From 3c43089cc25b19f068e5b1b7a9758010cbbcc14f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Feb 2022 17:03:18 -0600 Subject: [PATCH 159/298] Log traceback in debug for Sonos unsubscribe errors (#65596) --- homeassistant/components/sonos/speaker.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b5ae20e1123..e40fe901b09 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -400,7 +400,12 @@ class SonosSpeaker: ) for result in results: if isinstance(result, Exception): - _LOGGER.debug("Unsubscribe failed for %s: %s", self.zone_name, result) + _LOGGER.debug( + "Unsubscribe failed for %s: %s", + self.zone_name, + result, + exc_info=result, + ) self._subscriptions = [] @callback From e5b9d5baa31c5aecf6ede928100236a5089fc1a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Feb 2022 15:06:46 -0800 Subject: [PATCH 160/298] Bumped version to 2022.2.1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c6403514ac9..17f0d651eab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 970a736b68f..8f180fe2a6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.0 +version = 2022.2.1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 88c3ab11137feac7f31cf769ccfe310bc752725d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Feb 2022 17:44:19 -0600 Subject: [PATCH 161/298] Fix lutron_caseta button events including area name in device name (#65601) --- homeassistant/components/lutron_caseta/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d73e03b44d4..546bb055ca8 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -227,7 +227,7 @@ def _async_subscribe_pico_remote_events( action = ACTION_RELEASE type_ = device["type"] - name = device["name"] + area, name = device["name"].split("_", 1) button_number = device["button_number"] # The original implementation used LIP instead of LEAP # so we need to convert the button number to maintain compat @@ -252,7 +252,7 @@ def _async_subscribe_pico_remote_events( ATTR_BUTTON_NUMBER: lip_button_number, ATTR_LEAP_BUTTON_NUMBER: button_number, ATTR_DEVICE_NAME: name, - ATTR_AREA_NAME: name.split("_")[0], + ATTR_AREA_NAME: area, ATTR_ACTION: action, }, ) From b450a41d7b321639c2fd034b2cbd8e6d3b26d301 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 4 Feb 2022 02:19:36 +0200 Subject: [PATCH 162/298] Fix Shelly Plus i4 KeyError (#65604) --- homeassistant/components/shelly/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a01b5de133a..7a41c914e8a 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -264,7 +264,8 @@ def get_model_name(info: dict[str, Any]) -> str: def get_rpc_channel_name(device: RpcDevice, key: str) -> str: """Get name based on device and channel name.""" - key = key.replace("input", "switch") + if device.config.get("switch:0"): + key = key.replace("input", "switch") device_name = get_rpc_device_name(device) entity_name: str | None = device.config[key].get("name", device_name) From a670317b80306a10f38e490ae0b8169e9987dcfd Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Fri, 4 Feb 2022 09:51:34 +0100 Subject: [PATCH 163/298] Bumped boschshcpy 0.2.28 to 0.2.29 (#65328) --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 2ed89c0bf5b..98fd5ab2d27 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.28"], + "requirements": ["boschshcpy==0.2.29"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], diff --git a/requirements_all.txt b/requirements_all.txt index 42800f50eec..ab0f34968e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ blockchain==1.4.4 bond-api==0.1.16 # homeassistant.components.bosch_shc -boschshcpy==0.2.28 +boschshcpy==0.2.29 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83fed2a14e3..8ec4b86bc28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ blinkpy==0.18.0 bond-api==0.1.16 # homeassistant.components.bosch_shc -boschshcpy==0.2.28 +boschshcpy==0.2.29 # homeassistant.components.braviatv bravia-tv==1.0.11 From 3babc43fa521b483ed5dde0319c6ae7f6558e8e8 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 4 Feb 2022 18:12:35 +0100 Subject: [PATCH 164/298] Add migration to migrate 'homewizard_energy' to 'homewizard' (#65594) --- .../components/homewizard/__init__.py | 48 +++++++++- .../components/homewizard/config_flow.py | 26 +++++- .../components/homewizard/test_config_flow.py | 33 +++++++ tests/components/homewizard/test_init.py | 90 +++++++++++++++++++ 4 files changed, 193 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index bca041c6a27..b50d87a940d 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -3,10 +3,11 @@ import logging from aiohwenergy import DisabledError -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import UpdateFailed from .const import DOMAIN, PLATFORMS @@ -20,6 +21,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("__init__ async_setup_entry") + # Migrate `homewizard_energy` (custom_component) to `homewizard` + if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: + # Remove the old config entry ID from the entry data so we don't try this again + # on the next setup + data = entry.data.copy() + old_config_entry_id = data.pop("old_config_entry_id") + + hass.config_entries.async_update_entry(entry, data=data) + _LOGGER.debug( + ( + "Setting up imported homewizard_energy entry %s for the first time as " + "homewizard entry %s" + ), + old_config_entry_id, + entry.entry_id, + ) + + ent_reg = er.async_get(hass) + for entity in er.async_entries_for_config_entry(ent_reg, old_config_entry_id): + _LOGGER.debug("Removing %s", entity.entity_id) + ent_reg.async_remove(entity.entity_id) + + _LOGGER.debug("Re-creating %s for the new config entry", entity.entity_id) + # We will precreate the entity so that any customizations can be preserved + new_entity = ent_reg.async_get_or_create( + entity.domain, + DOMAIN, + entity.unique_id, + suggested_object_id=entity.entity_id.split(".")[1], + disabled_by=entity.disabled_by, + config_entry=entry, + original_name=entity.original_name, + original_icon=entity.original_icon, + ) + _LOGGER.debug("Re-created %s", new_entity.entity_id) + + # If there are customizations on the old entity, apply them to the new one + if entity.name or entity.icon: + ent_reg.async_update_entity( + new_entity.entity_id, name=entity.name, icon=entity.icon + ) + + # Remove the old config entry and now the entry is fully migrated + hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) + # Create coordinator coordinator = Coordinator(hass, entry.data[CONF_IP_ADDRESS]) try: diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 17f87680c62..45a912fefec 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -28,6 +28,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the HomeWizard config flow.""" self.config: dict[str, str | int] = {} + async def async_step_import(self, import_config: dict) -> FlowResult: + """Handle a flow initiated by older `homewizard_energy` component.""" + _LOGGER.debug("config_flow async_step_import") + + self.hass.components.persistent_notification.async_create( + ( + "The custom integration of HomeWizard Energy has been migrated to core. " + "You can safely remove the custom integration from the custom_integrations folder." + ), + "HomeWizard Energy", + f"homewizard_energy_to_{DOMAIN}", + ) + + return await self.async_step_user({CONF_IP_ADDRESS: import_config["host"]}) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -59,12 +74,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) + data: dict[str, str] = {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} + + if self.source == config_entries.SOURCE_IMPORT: + old_config_entry_id = self.context["old_config_entry_id"] + assert self.hass.config_entries.async_get_entry(old_config_entry_id) + data["old_config_entry_id"] = old_config_entry_id + # Add entry return self.async_create_entry( title=f"{device_info[CONF_PRODUCT_NAME]} ({device_info[CONF_SERIAL]})", - data={ - CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], - }, + data=data, ) async def async_step_zeroconf( diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 7364a0e632e..f416027da4a 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -12,6 +12,8 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ from .generator import get_mock_device +from tests.common import MockConfigEntry + _LOGGER = logging.getLogger(__name__) @@ -88,6 +90,37 @@ async def test_discovery_flow_works(hass, aioclient_mock): assert result["result"].unique_id == "HWE-P1_aabbccddeeff" +async def test_config_flow_imports_entry(aioclient_mock, hass): + """Test config flow accepts imported configuration.""" + + device = get_mock_device() + + mock_entry = MockConfigEntry(domain="homewizard_energy", data={"host": "1.2.3.4"}) + mock_entry.add_to_hass(hass) + + with patch("aiohwenergy.HomeWizardEnergy", return_value=device,), patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_IMPORT, + "old_config_entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{device.device.product_name} (aabbccddeeff)" + assert result["data"][CONF_IP_ADDRESS] == "1.2.3.4" + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(device.initialize.mock_calls) == 1 + assert len(device.close.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_discovery_disabled_api(hass, aioclient_mock): """Test discovery detecting disabled api.""" diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index f7aa4de7ade..87a02a446e9 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -4,9 +4,11 @@ from unittest.mock import patch from aiohwenergy import AiohwenergyException, DisabledError +from homeassistant import config_entries from homeassistant.components.homewizard.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -68,6 +70,94 @@ async def test_load_failed_host_unavailable(aioclient_mock, hass): assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): + """Test config flow accepts imported configuration.""" + + device = get_mock_device() + + # Add original entry + original_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4"}, + entry_id="old_id", + ) + original_entry.add_to_hass(hass) + + # Give it some entities to see of they migrate properly + ent_reg = er.async_get(hass) + old_entity_active_power = ent_reg.async_get_or_create( + "sensor", + "homewizard_energy", + "p1_active_power_unique_id", + config_entry=original_entry, + original_name="Active Power", + suggested_object_id="p1_active_power", + ) + old_entity_switch = ent_reg.async_get_or_create( + "switch", + "homewizard_energy", + "socket_switch_unique_id", + config_entry=original_entry, + original_name="Switch", + suggested_object_id="socket_switch", + ) + old_entity_disabled_sensor = ent_reg.async_get_or_create( + "sensor", + "homewizard_energy", + "socket_disabled_unique_id", + config_entry=original_entry, + original_name="Switch Disabled", + suggested_object_id="socket_disabled", + disabled_by=er.DISABLED_USER, + ) + # Update some user-customs + ent_reg.async_update_entity(old_entity_active_power.entity_id, name="new_name") + ent_reg.async_update_entity(old_entity_switch.entity_id, icon="new_icon") + + imported_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4", "old_config_entry_id": "old_id"}, + source=config_entries.SOURCE_IMPORT, + entry_id="new_id", + ) + imported_entry.add_to_hass(hass) + + # Add the entry_id to trigger migration + with patch( + "aiohwenergy.HomeWizardEnergy", + return_value=device, + ): + await hass.config_entries.async_setup(imported_entry.entry_id) + await hass.async_block_till_done() + + assert original_entry.state is ConfigEntryState.NOT_LOADED + assert imported_entry.state is ConfigEntryState.LOADED + + # Check if new entities are migrated + new_entity_active_power = ent_reg.async_get(old_entity_active_power.entity_id) + assert new_entity_active_power.platform == DOMAIN + assert new_entity_active_power.name == "new_name" + assert new_entity_active_power.icon is None + assert new_entity_active_power.original_name == "Active Power" + assert new_entity_active_power.unique_id == "p1_active_power_unique_id" + assert new_entity_active_power.disabled_by is None + + new_entity_switch = ent_reg.async_get(old_entity_switch.entity_id) + assert new_entity_switch.platform == DOMAIN + assert new_entity_switch.name is None + assert new_entity_switch.icon == "new_icon" + assert new_entity_switch.original_name == "Switch" + assert new_entity_switch.unique_id == "socket_switch_unique_id" + assert new_entity_switch.disabled_by is None + + new_entity_disabled_sensor = ent_reg.async_get(old_entity_disabled_sensor.entity_id) + assert new_entity_disabled_sensor.platform == DOMAIN + assert new_entity_disabled_sensor.name is None + assert new_entity_disabled_sensor.original_name == "Switch Disabled" + assert new_entity_disabled_sensor.unique_id == "socket_disabled_unique_id" + assert new_entity_disabled_sensor.disabled_by == er.DISABLED_USER + + async def test_load_detect_api_disabled(aioclient_mock, hass): """Test setup detects disabled API.""" From 9eeaec4f7980dc0ac902eb4c7cd94eaf27c35e1c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 4 Feb 2022 02:50:47 +0100 Subject: [PATCH 165/298] Raise when zwave_js device automation fails validation (#65610) --- .../components/zwave_js/device_condition.py | 11 ++++++++++- .../components/zwave_js/device_trigger.py | 11 ++++++++++- homeassistant/components/zwave_js/helpers.py | 3 ++- .../zwave_js/test_device_condition.py | 17 +++++++++++++++++ .../components/zwave_js/test_device_trigger.py | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 9840d89dc9d..fcd769dc8a4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -99,7 +99,16 @@ async def async_validate_condition_config( # We return early if the config entry for this device is not ready because we can't # validate the value without knowing the state of the device - if async_is_device_config_entry_not_loaded(hass, config[CONF_DEVICE_ID]): + try: + device_config_entry_not_loaded = async_is_device_config_entry_not_loaded( + hass, config[CONF_DEVICE_ID] + ) + except ValueError as err: + raise InvalidDeviceAutomationConfig( + f"Device {config[CONF_DEVICE_ID]} not found" + ) from err + + if device_config_entry_not_loaded: return config if config[CONF_TYPE] == VALUE_TYPE: diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 481fc429cb0..888efbf2bfd 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -217,7 +217,16 @@ async def async_validate_trigger_config( # We return early if the config entry for this device is not ready because we can't # validate the value without knowing the state of the device - if async_is_device_config_entry_not_loaded(hass, config[CONF_DEVICE_ID]): + try: + device_config_entry_not_loaded = async_is_device_config_entry_not_loaded( + hass, config[CONF_DEVICE_ID] + ) + except ValueError as err: + raise InvalidDeviceAutomationConfig( + f"Device {config[CONF_DEVICE_ID]} not found" + ) from err + + if device_config_entry_not_loaded: return config trigger_type = config[CONF_TYPE] diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 3f57f4bbe6f..de7ed5da502 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -298,7 +298,8 @@ def async_is_device_config_entry_not_loaded( """Return whether device's config entries are not loaded.""" dev_reg = dr.async_get(hass) device = dev_reg.async_get(device_id) - assert device + if device is None: + raise ValueError(f"Device {device_id} not found") return any( (entry := hass.config_entries.async_get_entry(entry_id)) and entry.state != ConfigEntryState.LOADED diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 3919edbd340..71a6865287c 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -596,6 +596,23 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration): == INVALID_CONFIG ) + # Test invalid device ID fails validation + with pytest.raises(InvalidDeviceAutomationConfig): + await device_condition.async_validate_condition_config( + hass, + { + "condition": "device", + "domain": DOMAIN, + "type": "value", + "device_id": "invalid_device_id", + "command_class": CommandClass.DOOR_LOCK.value, + "property": 9999, + "property_key": 9999, + "endpoint": 9999, + "value": 9999, + }, + ) + async def test_get_value_from_config_failure( hass, client, hank_binary_switch, integration diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 19c86af22ed..bf3738a7fb3 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -1370,3 +1370,19 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration): await device_trigger.async_validate_trigger_config(hass, INVALID_CONFIG) == INVALID_CONFIG ) + + # Test invalid device ID fails validation + with pytest.raises(InvalidDeviceAutomationConfig): + await device_trigger.async_validate_trigger_config( + hass, + { + "platform": "device", + "domain": DOMAIN, + "device_id": "invalid_device_id", + "type": "zwave_js.value_updated.value", + "command_class": CommandClass.DOOR_LOCK.value, + "property": 9999, + "property_key": 9999, + "endpoint": 9999, + }, + ) From 06b6b176db039533fa223c46d2b943d1368419de Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Feb 2022 17:08:19 -0800 Subject: [PATCH 166/298] Bump androidtv to 0.0.62 (#65440) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 5a515dd4816..f68cdb36271 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.4.0", - "androidtv[async]==0.0.61", + "androidtv[async]==0.0.62", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion", "@ollo69"], diff --git a/requirements_all.txt b/requirements_all.txt index ab0f34968e1..34e1b15d0b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -311,7 +311,7 @@ ambiclimate==0.2.1 amcrest==1.9.3 # homeassistant.components.androidtv -androidtv[async]==0.0.61 +androidtv[async]==0.0.62 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ec4b86bc28..d345a186e5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -237,7 +237,7 @@ amberelectric==1.0.3 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.61 +androidtv[async]==0.0.62 # homeassistant.components.apns apns2==0.3.0 From b004c5deb6cd8e3c538cbf05f96dd796fe7bb0c4 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 4 Feb 2022 11:13:08 -0800 Subject: [PATCH 167/298] Bump androidtv to 0.0.63 (fix MAC issues) (#65615) --- .../components/androidtv/config_flow.py | 8 ++ .../components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/androidtv/patchers.py | 12 ++ .../components/androidtv/test_media_player.py | 122 +++++++++++------- 6 files changed, 99 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index c346378fbc2..0ec37fdeb6f 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -124,6 +124,14 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return RESULT_CONN_ERROR, None dev_prop = aftv.device_properties + _LOGGER.info( + "Android TV at %s: %s = %r, %s = %r", + user_input[CONF_HOST], + PROP_ETHMAC, + dev_prop.get(PROP_ETHMAC), + PROP_WIFIMAC, + dev_prop.get(PROP_WIFIMAC), + ) unique_id = format_mac( dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "") ) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index f68cdb36271..89037a835ca 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.4.0", - "androidtv[async]==0.0.62", + "androidtv[async]==0.0.63", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion", "@ollo69"], diff --git a/requirements_all.txt b/requirements_all.txt index 34e1b15d0b5..6a46e29b8b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -311,7 +311,7 @@ ambiclimate==0.2.1 amcrest==1.9.3 # homeassistant.components.androidtv -androidtv[async]==0.0.62 +androidtv[async]==0.0.63 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d345a186e5d..61781c64d8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -237,7 +237,7 @@ amberelectric==1.0.3 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.62 +androidtv[async]==0.0.63 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index c92ac11ba4b..4411945c71b 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -185,3 +185,15 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", side_effect=ZeroDivisionError, ) + +PATCH_DEVICE_PROPERTIES = patch( + "androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties", + return_value={ + "manufacturer": "a", + "model": "b", + "serialno": "c", + "sw_version": "d", + "wifimac": "ab:cd:ef:gh:ij:kl", + "ethmac": None, + }, +) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 5326e48f7b9..e97de0fc928 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -157,8 +157,10 @@ async def test_setup_with_properties(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(response)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state is not None @@ -188,8 +190,9 @@ async def test_reconnect(hass, caplog, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -256,8 +259,10 @@ async def test_adb_shell_returns_none(hass, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -284,8 +289,10 @@ async def test_setup_with_adbkey(hass): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -317,8 +324,10 @@ async def test_sources(hass, config0): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -395,8 +404,10 @@ async def _test_exclude_sources(hass, config0, expected_sources): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -475,8 +486,10 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -701,8 +714,10 @@ async def test_setup_fail(hass, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + await hass.async_block_till_done() + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is None @@ -718,8 +733,9 @@ async def test_adb_command(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response @@ -747,8 +763,9 @@ async def test_adb_command_unicode_decode_error(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", @@ -776,8 +793,9 @@ async def test_adb_command_key(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response @@ -805,8 +823,9 @@ async def test_adb_command_get_properties(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict", @@ -834,8 +853,9 @@ async def test_learn_sendevent(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent", @@ -862,8 +882,9 @@ async def test_update_lock_not_acquired(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -897,8 +918,9 @@ async def test_download(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Failed download because path is not whitelisted with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull: @@ -943,8 +965,9 @@ async def test_upload(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Failed upload because path is not whitelisted with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push: @@ -987,8 +1010,9 @@ async def test_androidtv_volume_set(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5 @@ -1014,8 +1038,9 @@ async def test_get_image(hass, hass_ws_client): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -1090,8 +1115,9 @@ async def test_services_androidtv(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: await _test_service( @@ -1136,8 +1162,9 @@ async def test_services_firetv(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back") @@ -1152,8 +1179,9 @@ async def test_volume_mute(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True} @@ -1196,8 +1224,9 @@ async def test_connection_closed_on_ha_stop(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close" @@ -1220,8 +1249,9 @@ async def test_exception(hass): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patchers.PATCH_DEVICE_PROPERTIES: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) From c6d5a0842b9ffd14324fd8dc7937598ea3147a06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 00:18:10 -0800 Subject: [PATCH 168/298] Bump homematicip to 1.0.2 (#65620) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b41c7b06c74..1a078fa9c8d 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.1"], + "requirements": ["homematicip==1.0.2"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 6a46e29b8b9..4465275bdb3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==1.0.1 +homematicip==1.0.2 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61781c64d8b..5b4037fc9ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -552,7 +552,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==1.0.1 +homematicip==1.0.2 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 0efa276fca19c8104575b5f93d8f1a124f8a3902 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Feb 2022 02:12:29 -0600 Subject: [PATCH 169/298] Bump flux_led to 0.28.20 (#65621) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index ac324431ba6..e10bf72b8c3 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.17"], + "requirements": ["flux_led==0.28.20"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 4465275bdb3..0696bcfb7be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.17 +flux_led==0.28.20 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b4037fc9ed..9beb79b8339 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.17 +flux_led==0.28.20 # homeassistant.components.homekit fnvhash==0.1.0 From 67a9932c5c0313f4548a69378db35cff594f1af4 Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Fri, 4 Feb 2022 01:50:24 -0800 Subject: [PATCH 170/298] Fix Z-Wave lights (#65638) * Fix Z-Wave lights * Update tests --- homeassistant/components/zwave/light.py | 2 +- tests/components/zwave/test_light.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index a029fa35a65..ea2b34a874f 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -123,7 +123,7 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): self._state = None self._color_mode = None self._supported_color_modes = set() - self._supported_features = None + self._supported_features = 0 self._delay = delay self._refresh_value = refresh self._zw098 = None diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 35128ccc69a..74c541f4d5a 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -39,7 +39,7 @@ def test_get_device_detects_dimmer(mock_openzwave): device = light.get_device(node=node, values=values, node_config={}) assert isinstance(device, light.ZwaveDimmer) assert device.color_mode == COLOR_MODE_BRIGHTNESS - assert device.supported_features is None + assert device.supported_features == 0 assert device.supported_color_modes == {COLOR_MODE_BRIGHTNESS} @@ -52,7 +52,7 @@ def test_get_device_detects_colorlight(mock_openzwave): device = light.get_device(node=node, values=values, node_config={}) assert isinstance(device, light.ZwaveColorLight) assert device.color_mode == COLOR_MODE_RGB - assert device.supported_features is None + assert device.supported_features == 0 assert device.supported_color_modes == {COLOR_MODE_RGB} @@ -68,7 +68,7 @@ def test_get_device_detects_zw098(mock_openzwave): device = light.get_device(node=node, values=values, node_config={}) assert isinstance(device, light.ZwaveColorLight) assert device.color_mode == COLOR_MODE_RGB - assert device.supported_features is None + assert device.supported_features == 0 assert device.supported_color_modes == {COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB} @@ -84,7 +84,7 @@ def test_get_device_detects_rgbw_light(mock_openzwave): device.value_added() assert isinstance(device, light.ZwaveColorLight) assert device.color_mode == COLOR_MODE_RGBW - assert device.supported_features is None + assert device.supported_features == 0 assert device.supported_color_modes == {COLOR_MODE_RGBW} From 4e3cd1471a304ba7b2a0e5dae32c7b37f6817657 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Feb 2022 14:49:45 +0100 Subject: [PATCH 171/298] Remove limit of amount of duplicated statistics (#65641) --- .../components/recorder/statistics.py | 11 -- tests/components/recorder/test_statistics.py | 142 ------------------ 2 files changed, 153 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 347722be0a5..0bf10ca71c6 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -119,8 +119,6 @@ QUERY_STATISTIC_META_ID = [ StatisticsMeta.statistic_id, ] -MAX_DUPLICATES = 1000000 - STATISTICS_BAKERY = "recorder_statistics_bakery" STATISTICS_META_BAKERY = "recorder_statistics_meta_bakery" STATISTICS_SHORT_TERM_BAKERY = "recorder_statistics_short_term_bakery" @@ -351,8 +349,6 @@ def _delete_duplicates_from_table( .delete(synchronize_session=False) ) total_deleted_rows += deleted_rows - if total_deleted_rows >= MAX_DUPLICATES: - break return (total_deleted_rows, all_non_identical_duplicates) @@ -389,13 +385,6 @@ def delete_duplicates(instance: Recorder, session: scoped_session) -> None: backup_path, ) - if deleted_statistics_rows >= MAX_DUPLICATES: - _LOGGER.warning( - "Found more than %s duplicated statistic rows, please report at " - 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+', - MAX_DUPLICATES - 1, - ) - deleted_short_term_statistics_rows, _ = _delete_duplicates_from_table( session, StatisticsShortTerm ) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 296409d984f..25590c712d9 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -852,7 +852,6 @@ def test_delete_duplicates(caplog, tmpdir): assert "Deleted 2 duplicated statistics rows" in caplog.text assert "Found non identical" not in caplog.text - assert "Found more than" not in caplog.text assert "Found duplicated" not in caplog.text @@ -989,7 +988,6 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): assert "Deleted 2 duplicated statistics rows" in caplog.text assert "Deleted 1 non identical" in caplog.text - assert "Found more than" not in caplog.text assert "Found duplicated" not in caplog.text isotime = dt_util.utcnow().isoformat() @@ -1028,144 +1026,6 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): ] -@patch.object(statistics, "MAX_DUPLICATES", 2) -def test_delete_duplicates_too_many(caplog, tmpdir): - """Test removal of duplicated statistics.""" - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.models_schema_23" - importlib.import_module(module) - old_models = sys.modules[module] - - period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) - period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) - period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) - period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) - - external_energy_statistics_1 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 2, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 3, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 4, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - ) - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_energy_statistics_2 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 20, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 30, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 40, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - ) - external_energy_metadata_2 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", - } - - # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.create_engine", new=_create_engine_test - ): - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) - ) - with session_scope(hass=hass) as session: - for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) - for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) - - hass.stop() - - # Test that the duplicates are removed during migration from schema 23 - hass = get_test_home_assistant() - hass.config.config_dir = tmpdir - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() - - assert "Deleted 2 duplicated statistics rows" in caplog.text - assert "Found non identical" not in caplog.text - assert "Found more than 1 duplicated statistic rows" in caplog.text - assert "Found duplicated" not in caplog.text - - -@patch.object(statistics, "MAX_DUPLICATES", 2) def test_delete_duplicates_short_term(caplog, tmpdir): """Test removal of duplicated statistics.""" test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") @@ -1228,7 +1088,6 @@ def test_delete_duplicates_short_term(caplog, tmpdir): assert "duplicated statistics rows" not in caplog.text assert "Found non identical" not in caplog.text - assert "Found more than" not in caplog.text assert "Deleted duplicated short term statistic" in caplog.text @@ -1240,7 +1099,6 @@ def test_delete_duplicates_no_duplicates(hass_recorder, caplog): delete_duplicates(hass.data[DATA_INSTANCE], session) assert "duplicated statistics rows" not in caplog.text assert "Found non identical" not in caplog.text - assert "Found more than" not in caplog.text assert "Found duplicated" not in caplog.text From 9cd6bb73350114ed7cbe288b2ec8aa4dde6148c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Feb 2022 18:55:11 +0100 Subject: [PATCH 172/298] Don't use shared session during recorder migration (#65672) --- .../components/recorder/migration.py | 297 ++++++++++-------- tests/components/recorder/test_migrate.py | 22 +- 2 files changed, 176 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 32119b85597..b49aee29ba1 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -68,20 +68,18 @@ def schema_is_current(current_version): def migrate_schema(instance, current_version): """Check if the schema needs to be upgraded.""" - with session_scope(session=instance.get_session()) as session: - _LOGGER.warning( - "Database is about to upgrade. Schema version: %s", current_version - ) - for version in range(current_version, SCHEMA_VERSION): - new_version = version + 1 - _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update(instance, session, new_version, current_version) + _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", new_version) + _apply_update(instance, new_version, current_version) + with session_scope(session=instance.get_session()) as session: session.add(SchemaChanges(schema_version=new_version)) - _LOGGER.info("Upgrade to version %s done", new_version) + _LOGGER.info("Upgrade to version %s done", new_version) -def _create_index(connection, table_name, index_name): +def _create_index(instance, table_name, index_name): """Create an index for the specified table. The index name should match the name given for the index @@ -103,7 +101,9 @@ def _create_index(connection, table_name, index_name): index_name, ) try: - index.create(connection) + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + index.create(connection) except (InternalError, ProgrammingError, OperationalError) as err: raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( @@ -113,7 +113,7 @@ def _create_index(connection, table_name, index_name): _LOGGER.debug("Finished creating %s", index_name) -def _drop_index(connection, table_name, index_name): +def _drop_index(instance, table_name, index_name): """Drop an index from a specified table. There is no universal way to do something like `DROP INDEX IF EXISTS` @@ -129,7 +129,9 @@ def _drop_index(connection, table_name, index_name): # Engines like DB2/Oracle try: - connection.execute(text(f"DROP INDEX {index_name}")) + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute(text(f"DROP INDEX {index_name}")) except SQLAlchemyError: pass else: @@ -138,13 +140,15 @@ def _drop_index(connection, table_name, index_name): # Engines like SQLite, SQL Server if not success: try: - connection.execute( - text( - "DROP INDEX {table}.{index}".format( - index=index_name, table=table_name + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + text( + "DROP INDEX {table}.{index}".format( + index=index_name, table=table_name + ) ) ) - ) except SQLAlchemyError: pass else: @@ -153,13 +157,15 @@ def _drop_index(connection, table_name, index_name): if not success: # Engines like MySQL, MS Access try: - connection.execute( - text( - "DROP INDEX {index} ON {table}".format( - index=index_name, table=table_name + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + text( + "DROP INDEX {index} ON {table}".format( + index=index_name, table=table_name + ) ) ) - ) except SQLAlchemyError: pass else: @@ -184,7 +190,7 @@ def _drop_index(connection, table_name, index_name): ) -def _add_columns(connection, table_name, columns_def): +def _add_columns(instance, table_name, columns_def): """Add columns to a table.""" _LOGGER.warning( "Adding columns %s to table %s. Note: this can take several " @@ -197,14 +203,16 @@ def _add_columns(connection, table_name, columns_def): columns_def = [f"ADD {col_def}" for col_def in columns_def] try: - connection.execute( - text( - "ALTER TABLE {table} {columns_def}".format( - table=table_name, columns_def=", ".join(columns_def) + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + text( + "ALTER TABLE {table} {columns_def}".format( + table=table_name, columns_def=", ".join(columns_def) + ) ) ) - ) - return + return except (InternalError, OperationalError): # Some engines support adding all columns at once, # this error is when they don't @@ -212,13 +220,15 @@ def _add_columns(connection, table_name, columns_def): for column_def in columns_def: try: - connection.execute( - text( - "ALTER TABLE {table} {column_def}".format( - table=table_name, column_def=column_def + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + text( + "ALTER TABLE {table} {column_def}".format( + table=table_name, column_def=column_def + ) ) ) - ) except (InternalError, OperationalError) as err: raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( @@ -228,7 +238,7 @@ def _add_columns(connection, table_name, columns_def): ) -def _modify_columns(connection, engine, table_name, columns_def): +def _modify_columns(instance, engine, table_name, columns_def): """Modify columns in a table.""" if engine.dialect.name == "sqlite": _LOGGER.debug( @@ -261,33 +271,37 @@ def _modify_columns(connection, engine, table_name, columns_def): columns_def = [f"MODIFY {col_def}" for col_def in columns_def] try: - connection.execute( - text( - "ALTER TABLE {table} {columns_def}".format( - table=table_name, columns_def=", ".join(columns_def) + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + text( + "ALTER TABLE {table} {columns_def}".format( + table=table_name, columns_def=", ".join(columns_def) + ) ) ) - ) - return + return except (InternalError, OperationalError): _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") for column_def in columns_def: try: - connection.execute( - text( - "ALTER TABLE {table} {column_def}".format( - table=table_name, column_def=column_def + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + text( + "ALTER TABLE {table} {column_def}".format( + table=table_name, column_def=column_def + ) ) ) - ) except (InternalError, OperationalError): _LOGGER.exception( "Could not modify column %s in table %s", column_def, table_name ) -def _update_states_table_with_foreign_key_options(connection, engine): +def _update_states_table_with_foreign_key_options(instance, engine): """Add the options to foreign key constraints.""" inspector = sqlalchemy.inspect(engine) alters = [] @@ -316,17 +330,19 @@ def _update_states_table_with_foreign_key_options(connection, engine): for alter in alters: try: - connection.execute(DropConstraint(alter["old_fk"])) - for fkc in states_key_constraints: - if fkc.column_keys == alter["columns"]: - connection.execute(AddConstraint(fkc)) + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute(DropConstraint(alter["old_fk"])) + for fkc in states_key_constraints: + if fkc.column_keys == alter["columns"]: + connection.execute(AddConstraint(fkc)) except (InternalError, OperationalError): _LOGGER.exception( "Could not update foreign options in %s table", TABLE_STATES ) -def _drop_foreign_key_constraints(connection, engine, table, columns): +def _drop_foreign_key_constraints(instance, engine, table, columns): """Drop foreign key constraints for a table on specific columns.""" inspector = sqlalchemy.inspect(engine) drops = [] @@ -345,7 +361,9 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): for drop in drops: try: - connection.execute(DropConstraint(drop)) + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute(DropConstraint(drop)) except (InternalError, OperationalError): _LOGGER.exception( "Could not drop foreign constraints in %s table on %s", @@ -354,17 +372,16 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): ) -def _apply_update(instance, session, new_version, old_version): # noqa: C901 +def _apply_update(instance, new_version, old_version): # noqa: C901 """Perform operations to bring schema up to date.""" engine = instance.engine - connection = session.connection() if new_version == 1: - _create_index(connection, "events", "ix_events_time_fired") + _create_index(instance, "events", "ix_events_time_fired") elif new_version == 2: # Create compound start/end index for recorder_runs - _create_index(connection, "recorder_runs", "ix_recorder_runs_start_end") + _create_index(instance, "recorder_runs", "ix_recorder_runs_start_end") # Create indexes for states - _create_index(connection, "states", "ix_states_last_updated") + _create_index(instance, "states", "ix_states_last_updated") elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -374,41 +391,41 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 if old_version == 3: # Remove index that was added in version 3 - _drop_index(connection, "states", "ix_states_created_domain") + _drop_index(instance, "states", "ix_states_created_domain") if old_version == 2: # Remove index that was added in version 2 - _drop_index(connection, "states", "ix_states_entity_id_created") + _drop_index(instance, "states", "ix_states_entity_id_created") # Remove indexes that were added in version 0 - _drop_index(connection, "states", "states__state_changes") - _drop_index(connection, "states", "states__significant_changes") - _drop_index(connection, "states", "ix_states_entity_id_created") + _drop_index(instance, "states", "states__state_changes") + _drop_index(instance, "states", "states__significant_changes") + _drop_index(instance, "states", "ix_states_entity_id_created") - _create_index(connection, "states", "ix_states_entity_id_last_updated") + _create_index(instance, "states", "ix_states_entity_id_last_updated") elif new_version == 5: # Create supporting index for States.event_id foreign key - _create_index(connection, "states", "ix_states_event_id") + _create_index(instance, "states", "ix_states_event_id") elif new_version == 6: _add_columns( - session, + instance, "events", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(connection, "events", "ix_events_context_id") - _create_index(connection, "events", "ix_events_context_user_id") + _create_index(instance, "events", "ix_events_context_id") + _create_index(instance, "events", "ix_events_context_user_id") _add_columns( - connection, + instance, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(connection, "states", "ix_states_context_id") - _create_index(connection, "states", "ix_states_context_user_id") + _create_index(instance, "states", "ix_states_context_id") + _create_index(instance, "states", "ix_states_context_user_id") elif new_version == 7: - _create_index(connection, "states", "ix_states_entity_id") + _create_index(instance, "states", "ix_states_entity_id") elif new_version == 8: - _add_columns(connection, "events", ["context_parent_id CHARACTER(36)"]) - _add_columns(connection, "states", ["old_state_id INTEGER"]) - _create_index(connection, "events", "ix_events_context_parent_id") + _add_columns(instance, "events", ["context_parent_id CHARACTER(36)"]) + _add_columns(instance, "states", ["old_state_id INTEGER"]) + _create_index(instance, "events", "ix_events_context_parent_id") elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -418,36 +435,36 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 # and we would have to move to something like # sqlalchemy alembic to make that work # - _drop_index(connection, "states", "ix_states_context_id") - _drop_index(connection, "states", "ix_states_context_user_id") + _drop_index(instance, "states", "ix_states_context_id") + _drop_index(instance, "states", "ix_states_context_user_id") # This index won't be there if they were not running # nightly but we don't treat that as a critical issue - _drop_index(connection, "states", "ix_states_context_parent_id") + _drop_index(instance, "states", "ix_states_context_parent_id") # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated - _drop_index(connection, "states", "ix_states_entity_id") - _create_index(connection, "events", "ix_events_event_type_time_fired") - _drop_index(connection, "events", "ix_events_event_type") + _drop_index(instance, "states", "ix_states_entity_id") + _create_index(instance, "events", "ix_events_event_type_time_fired") + _drop_index(instance, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 pass elif new_version == 11: - _create_index(connection, "states", "ix_states_old_state_id") - _update_states_table_with_foreign_key_options(connection, engine) + _create_index(instance, "states", "ix_states_old_state_id") + _update_states_table_with_foreign_key_options(instance, engine) elif new_version == 12: if engine.dialect.name == "mysql": - _modify_columns(connection, engine, "events", ["event_data LONGTEXT"]) - _modify_columns(connection, engine, "states", ["attributes LONGTEXT"]) + _modify_columns(instance, engine, "events", ["event_data LONGTEXT"]) + _modify_columns(instance, engine, "states", ["attributes LONGTEXT"]) elif new_version == 13: if engine.dialect.name == "mysql": _modify_columns( - connection, + instance, engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"], ) _modify_columns( - connection, + instance, engine, "states", [ @@ -457,14 +474,12 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 ], ) elif new_version == 14: - _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) + _modify_columns(instance, engine, "events", ["event_type VARCHAR(64)"]) elif new_version == 15: # This dropped the statistics table, done again in version 18. pass elif new_version == 16: - _drop_foreign_key_constraints( - connection, engine, TABLE_STATES, ["old_state_id"] - ) + _drop_foreign_key_constraints(instance, engine, TABLE_STATES, ["old_state_id"]) elif new_version == 17: # This dropped the statistics table, done again in version 18. pass @@ -489,12 +504,13 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 elif new_version == 19: # This adds the statistic runs table, insert a fake run to prevent duplicating # statistics. - session.add(StatisticsRuns(start=get_start_time())) + with session_scope(session=instance.get_session()) as session: + session.add(StatisticsRuns(start=get_start_time())) elif new_version == 20: # This changed the precision of statistics from float to double if engine.dialect.name in ["mysql", "postgresql"]: _modify_columns( - connection, + instance, engine, "statistics", [ @@ -516,14 +532,16 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 table, ) with contextlib.suppress(SQLAlchemyError): - connection.execute( - # Using LOCK=EXCLUSIVE to prevent the database from corrupting - # https://github.com/home-assistant/core/issues/56104 - text( - f"ALTER TABLE {table} CONVERT TO " - "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci LOCK=EXCLUSIVE" + with session_scope(session=instance.get_session()) as session: + connection = session.connection() + connection.execute( + # Using LOCK=EXCLUSIVE to prevent the database from corrupting + # https://github.com/home-assistant/core/issues/56104 + text( + f"ALTER TABLE {table} CONVERT TO " + "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci LOCK=EXCLUSIVE" + ) ) - ) elif new_version == 22: # Recreate the all statistics tables for Oracle DB with Identity columns # @@ -549,57 +567,64 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 # Block 5-minute statistics for one hour from the last run, or it will overlap # with existing hourly statistics. Don't block on a database with no existing # statistics. - if session.query(Statistics.id).count() and ( - last_run_string := session.query(func.max(StatisticsRuns.start)).scalar() - ): - last_run_start_time = process_timestamp(last_run_string) - if last_run_start_time: - fake_start_time = last_run_start_time + timedelta(minutes=5) - while fake_start_time < last_run_start_time + timedelta(hours=1): - session.add(StatisticsRuns(start=fake_start_time)) - fake_start_time += timedelta(minutes=5) + with session_scope(session=instance.get_session()) as session: + if session.query(Statistics.id).count() and ( + last_run_string := session.query( + func.max(StatisticsRuns.start) + ).scalar() + ): + last_run_start_time = process_timestamp(last_run_string) + if last_run_start_time: + fake_start_time = last_run_start_time + timedelta(minutes=5) + while fake_start_time < last_run_start_time + timedelta(hours=1): + session.add(StatisticsRuns(start=fake_start_time)) + fake_start_time += timedelta(minutes=5) # When querying the database, be careful to only explicitly query for columns # which were present in schema version 21. If querying the table, SQLAlchemy # will refer to future columns. - for sum_statistic in session.query(StatisticsMeta.id).filter_by(has_sum=true()): - last_statistic = ( - session.query( - Statistics.start, - Statistics.last_reset, - Statistics.state, - Statistics.sum, - ) - .filter_by(metadata_id=sum_statistic.id) - .order_by(Statistics.start.desc()) - .first() - ) - if last_statistic: - session.add( - StatisticsShortTerm( - metadata_id=sum_statistic.id, - start=last_statistic.start, - last_reset=last_statistic.last_reset, - state=last_statistic.state, - sum=last_statistic.sum, + with session_scope(session=instance.get_session()) as session: + for sum_statistic in session.query(StatisticsMeta.id).filter_by( + has_sum=true() + ): + last_statistic = ( + session.query( + Statistics.start, + Statistics.last_reset, + Statistics.state, + Statistics.sum, ) + .filter_by(metadata_id=sum_statistic.id) + .order_by(Statistics.start.desc()) + .first() ) + if last_statistic: + session.add( + StatisticsShortTerm( + metadata_id=sum_statistic.id, + start=last_statistic.start, + last_reset=last_statistic.last_reset, + state=last_statistic.state, + sum=last_statistic.sum, + ) + ) elif new_version == 23: # Add name column to StatisticsMeta - _add_columns(session, "statistics_meta", ["name VARCHAR(255)"]) + _add_columns(instance, "statistics_meta", ["name VARCHAR(255)"]) elif new_version == 24: # Delete duplicated statistics - delete_duplicates(instance, session) + with session_scope(session=instance.get_session()) as session: + delete_duplicates(instance, session) # Recreate statistics indices to block duplicated statistics - _drop_index(connection, "statistics", "ix_statistics_statistic_id_start") - _create_index(connection, "statistics", "ix_statistics_statistic_id_start") + _drop_index(instance, "statistics", "ix_statistics_statistic_id_start") + _create_index(instance, "statistics", "ix_statistics_statistic_id_start") _drop_index( - connection, + instance, "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) _create_index( - connection, + instance, "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 5c8a1c556c9..5e837eb36ac 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -5,7 +5,7 @@ import importlib import sqlite3 import sys import threading -from unittest.mock import ANY, Mock, PropertyMock, call, patch +from unittest.mock import Mock, PropertyMock, call, patch import pytest from sqlalchemy import create_engine, text @@ -57,7 +57,7 @@ async def test_schema_update_calls(hass): assert recorder.util.async_migration_in_progress(hass) is False update.assert_has_calls( [ - call(hass.data[DATA_INSTANCE], ANY, version + 1, 0) + call(hass.data[DATA_INSTANCE], version + 1, 0) for version in range(0, models.SCHEMA_VERSION) ] ) @@ -309,7 +309,7 @@ async def test_schema_migrate(hass, start_version): def test_invalid_update(): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): - migration._apply_update(Mock(), Mock(), -1, 0) + migration._apply_update(Mock(), -1, 0) @pytest.mark.parametrize( @@ -324,9 +324,13 @@ def test_invalid_update(): def test_modify_column(engine_type, substr): """Test that modify column generates the expected query.""" connection = Mock() + session = Mock() + session.connection = Mock(return_value=connection) + instance = Mock() + instance.get_session = Mock(return_value=session) engine = Mock() engine.dialect.name = engine_type - migration._modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) + migration._modify_columns(instance, engine, "events", ["event_type VARCHAR(64)"]) if substr: assert substr in connection.execute.call_args[0][0].text else: @@ -338,8 +342,10 @@ def test_forgiving_add_column(): engine = create_engine("sqlite://", poolclass=StaticPool) with Session(engine) as session: session.execute(text("CREATE TABLE hello (id int)")) - migration._add_columns(session, "hello", ["context_id CHARACTER(36)"]) - migration._add_columns(session, "hello", ["context_id CHARACTER(36)"]) + instance = Mock() + instance.get_session = Mock(return_value=session) + migration._add_columns(instance, "hello", ["context_id CHARACTER(36)"]) + migration._add_columns(instance, "hello", ["context_id CHARACTER(36)"]) def test_forgiving_add_index(): @@ -347,7 +353,9 @@ def test_forgiving_add_index(): engine = create_engine("sqlite://", poolclass=StaticPool) models.Base.metadata.create_all(engine) with Session(engine) as session: - migration._create_index(session, "states", "ix_states_context_id") + instance = Mock() + instance.get_session = Mock(return_value=session) + migration._create_index(instance, "states", "ix_states_context_id") @pytest.mark.parametrize( From ea1245f308a271136ba6cf523e2779c6bce73072 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Feb 2022 19:55:28 +0100 Subject: [PATCH 173/298] Improve recorder migration for PostgreSQL when columns already exist (#65680) --- homeassistant/components/recorder/migration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index b49aee29ba1..cb94018b1b1 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -104,7 +104,7 @@ def _create_index(instance, table_name, index_name): with session_scope(session=instance.get_session()) as session: connection = session.connection() index.create(connection) - except (InternalError, ProgrammingError, OperationalError) as err: + except (InternalError, OperationalError, ProgrammingError) as err: raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( "Index %s already exists on %s, continuing", index_name, table_name @@ -213,7 +213,7 @@ def _add_columns(instance, table_name, columns_def): ) ) return - except (InternalError, OperationalError): + except (InternalError, OperationalError, ProgrammingError): # Some engines support adding all columns at once, # this error is when they don't _LOGGER.info("Unable to use quick column add. Adding 1 by 1") @@ -229,7 +229,7 @@ def _add_columns(instance, table_name, columns_def): ) ) ) - except (InternalError, OperationalError) as err: + except (InternalError, OperationalError, ProgrammingError) as err: raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( "Column %s already exists on %s, continuing", From e6e95a1131cf7cc79054c9e0eb0ae7896aefb0c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Feb 2022 20:31:12 +0100 Subject: [PATCH 174/298] Only remove duplicated statistics on error (#65653) --- .../components/recorder/migration.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index cb94018b1b1..b8f15a811db 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -6,6 +6,7 @@ import logging import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text from sqlalchemy.exc import ( + DatabaseError, InternalError, OperationalError, ProgrammingError, @@ -612,22 +613,31 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 # Add name column to StatisticsMeta _add_columns(instance, "statistics_meta", ["name VARCHAR(255)"]) elif new_version == 24: - # Delete duplicated statistics - with session_scope(session=instance.get_session()) as session: - delete_duplicates(instance, session) # Recreate statistics indices to block duplicated statistics _drop_index(instance, "statistics", "ix_statistics_statistic_id_start") - _create_index(instance, "statistics", "ix_statistics_statistic_id_start") _drop_index( instance, "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) - _create_index( - instance, - "statistics_short_term", - "ix_statistics_short_term_statistic_id_start", - ) + try: + _create_index(instance, "statistics", "ix_statistics_statistic_id_start") + _create_index( + instance, + "statistics_short_term", + "ix_statistics_short_term_statistic_id_start", + ) + except DatabaseError: + # There may be duplicated statistics entries, delete duplicated statistics + # and try again + with session_scope(session=instance.get_session()) as session: + delete_duplicates(instance, session) + _create_index(instance, "statistics", "ix_statistics_statistic_id_start") + _create_index( + instance, + "statistics_short_term", + "ix_statistics_short_term_statistic_id_start", + ) else: raise ValueError(f"No schema migration defined for version {new_version}") From 35f2536d462aa25aa95e3747af953011aa5da739 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 4 Feb 2022 16:30:57 +0100 Subject: [PATCH 175/298] Bump renault-api to 0.1.8 (#65670) Co-authored-by: epenet --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 9442ea8160b..0b0c7d98164 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", "requirements": [ - "renault-api==0.1.7" + "renault-api==0.1.8" ], "codeowners": [ "@epenet" diff --git a/requirements_all.txt b/requirements_all.txt index 0696bcfb7be..4d5cd66a184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2087,7 +2087,7 @@ raspyrfm-client==1.2.8 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.7 +renault-api==0.1.8 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9beb79b8339..6932cdbcd47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1282,7 +1282,7 @@ rachiopy==1.0.3 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.7 +renault-api==0.1.8 # homeassistant.components.python_script restrictedpython==5.2 From 84b2ec224418ef74fa9e98c5d6a23bdf8b70cced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Feb 2022 13:36:30 -0600 Subject: [PATCH 176/298] Fix warm/cold reversal in rgbww_to_color_temperature (#65677) --- homeassistant/util/color.py | 26 ++++++- tests/components/light/test_init.py | 61 +++++++++++++++- tests/util/test_color.py | 105 ++++++++++++++++++++++++++-- 3 files changed, 180 insertions(+), 12 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index f308595adbd..f055a5f32eb 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -531,13 +531,33 @@ def color_temperature_to_rgb( def color_temperature_to_rgbww( temperature: int, brightness: int, min_mireds: int, max_mireds: int ) -> tuple[int, int, int, int, int]: - """Convert color temperature to rgbcw.""" + """Convert color temperature in mireds to rgbcw.""" mired_range = max_mireds - min_mireds - warm = ((max_mireds - temperature) / mired_range) * brightness - cold = brightness - warm + cold = ((max_mireds - temperature) / mired_range) * brightness + warm = brightness - cold return (0, 0, 0, round(cold), round(warm)) +def rgbww_to_color_temperature( + rgbww: tuple[int, int, int, int, int], min_mireds: int, max_mireds: int +) -> tuple[int, int]: + """Convert rgbcw to color temperature in mireds.""" + _, _, _, cold, warm = rgbww + return while_levels_to_color_temperature(cold, warm, min_mireds, max_mireds) + + +def while_levels_to_color_temperature( + cold: int, warm: int, min_mireds: int, max_mireds: int +) -> tuple[int, int]: + """Convert whites to color temperature in mireds.""" + brightness = warm / 255 + cold / 255 + if brightness == 0: + return (max_mireds, 0) + return round( + ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds + ), min(255, round(brightness * 255)) + + def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: """ Clamp the given color component value between the given min and max values. diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index a8a6ebc901e..640a4b4533b 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1899,7 +1899,8 @@ async def test_light_service_call_color_temp_conversion( _, data = entity0.last_call("turn_on") assert data == {"brightness": 255, "color_temp": 153} _, data = entity1.last_call("turn_on") - assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 0, 255)} + # Home Assistant uses RGBCW so a mireds of 153 should be maximum cold at 100% brightness so 255 + assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 255, 0)} await hass.services.async_call( "light", @@ -1917,7 +1918,63 @@ async def test_light_service_call_color_temp_conversion( _, data = entity0.last_call("turn_on") assert data == {"brightness": 128, "color_temp": 500} _, data = entity1.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (0, 0, 0, 128, 0)} + # Home Assistant uses RGBCW so a mireds of 500 should be maximum warm at 50% brightness so 128 + assert data == {"brightness": 128, "rgbww_color": (0, 0, 0, 0, 128)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + ], + "brightness_pct": 100, + "color_temp": 327, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "color_temp": 327} + _, data = entity1.last_call("turn_on") + # Home Assistant uses RGBCW so a mireds of 328 should be the midway point at 100% brightness so 127 (rounding), 128 + assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 127, 128)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + ], + "brightness_pct": 100, + "color_temp": 240, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "color_temp": 240} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 191, 64)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + ], + "brightness_pct": 100, + "color_temp": 410, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "color_temp": 410} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 66, 189)} async def test_light_service_call_white_mode(hass, enable_custom_integrations): diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 31778781676..0b1b8f7d17f 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -406,46 +406,137 @@ def test_color_rgb_to_rgbww(): def test_color_temperature_to_rgbww(): - """Test color temp to warm, cold conversion.""" + """Test color temp to warm, cold conversion. + + Temperature values must be in mireds + Home Assistant uses rgbcw for rgbww + """ assert color_util.color_temperature_to_rgbww(153, 255, 153, 500) == ( 0, 0, 0, - 0, 255, + 0, ) assert color_util.color_temperature_to_rgbww(153, 128, 153, 500) == ( 0, 0, 0, - 0, 128, + 0, ) assert color_util.color_temperature_to_rgbww(500, 255, 153, 500) == ( 0, 0, 0, - 255, 0, + 255, ) assert color_util.color_temperature_to_rgbww(500, 128, 153, 500) == ( 0, 0, 0, - 128, 0, + 128, ) assert color_util.color_temperature_to_rgbww(347, 255, 153, 500) == ( 0, 0, 0, - 143, 112, + 143, ) assert color_util.color_temperature_to_rgbww(347, 128, 153, 500) == ( 0, 0, 0, - 72, 56, + 72, + ) + + +def test_rgbww_to_color_temperature(): + """Test rgbww conversion to color temp. + + Temperature values must be in mireds + Home Assistant uses rgbcw for rgbww + """ + assert ( + color_util.rgbww_to_color_temperature( + ( + 0, + 0, + 0, + 255, + 0, + ), + 153, + 500, + ) + == (153, 255) + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 128, 0), 153, 500) == ( + 153, + 128, + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 255), 153, 500) == ( + 500, + 255, + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 128), 153, 500) == ( + 500, + 128, + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 112, 143), 153, 500) == ( + 348, + 255, + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 56, 72), 153, 500) == ( + 348, + 128, + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 0), 153, 500) == ( + 500, + 0, + ) + + +def test_white_levels_to_color_temperature(): + """Test warm, cold conversion to color temp. + + Temperature values must be in mireds + Home Assistant uses rgbcw for rgbww + """ + assert ( + color_util.while_levels_to_color_temperature( + 255, + 0, + 153, + 500, + ) + == (153, 255) + ) + assert color_util.while_levels_to_color_temperature(128, 0, 153, 500) == ( + 153, + 128, + ) + assert color_util.while_levels_to_color_temperature(0, 255, 153, 500) == ( + 500, + 255, + ) + assert color_util.while_levels_to_color_temperature(0, 128, 153, 500) == ( + 500, + 128, + ) + assert color_util.while_levels_to_color_temperature(112, 143, 153, 500) == ( + 348, + 255, + ) + assert color_util.while_levels_to_color_temperature(56, 72, 153, 500) == ( + 348, + 128, + ) + assert color_util.while_levels_to_color_temperature(0, 0, 153, 500) == ( + 500, + 0, ) From 5aa02b884e73fda116d21e74dec554c06367e1c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 09:57:14 -0800 Subject: [PATCH 177/298] Call out 3rd party containers more clearly (#65684) --- homeassistant/helpers/system_info.py | 5 ++++- tests/helpers/test_system_info.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index e137d0f673e..a551c6e3b9e 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -41,8 +41,11 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: # Determine installation type on current data if info_object["docker"]: - if info_object["user"] == "root": + if info_object["user"] == "root" and os.path.isfile("/OFFICIAL_IMAGE"): info_object["installation_type"] = "Home Assistant Container" + else: + info_object["installation_type"] = "Unsupported Third Party Container" + elif is_virtual_env(): info_object["installation_type"] = "Home Assistant Core" diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index f4cb70f421a..e4aba5fbb24 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -18,15 +18,15 @@ async def test_container_installationtype(hass): """Test container installation type.""" with patch("platform.system", return_value="Linux"), patch( "os.path.isfile", return_value=True - ): + ), patch("homeassistant.helpers.system_info.getuser", return_value="root"): info = await hass.helpers.system_info.async_get_system_info() assert info["installation_type"] == "Home Assistant Container" with patch("platform.system", return_value="Linux"), patch( - "os.path.isfile", return_value=True + "os.path.isfile", side_effect=lambda file: file == "/.dockerenv" ), patch("homeassistant.helpers.system_info.getuser", return_value="user"): info = await hass.helpers.system_info.async_get_system_info() - assert info["installation_type"] == "Unknown" + assert info["installation_type"] == "Unsupported Third Party Container" async def test_getuser_keyerror(hass): From 6cf26652000da461771d178a067dc15aef5eb7c2 Mon Sep 17 00:00:00 2001 From: jkuettner <12213711+jkuettner@users.noreply.github.com> Date: Fri, 4 Feb 2022 18:47:31 +0100 Subject: [PATCH 178/298] Fix "vevent" KeyError in caldav component again (#65685) * Fix "vevent" KeyError in caldav component again * code formatting --- homeassistant/components/caldav/calendar.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index e9e1657065d..f44a59f18eb 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -232,7 +232,11 @@ class WebDavCalendarData: new_events.append(new_event) elif _start_of_tomorrow <= start_dt: break - vevents = [event.instance.vevent for event in results + new_events] + vevents = [ + event.instance.vevent + for event in results + new_events + if hasattr(event.instance, "vevent") + ] # dtstart can be a date or datetime depending if the event lasts a # whole day. Convert everything to datetime to be able to sort it From 27dbf98daefd58ae578f0dd3e741f77bafb1c301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 4 Feb 2022 19:33:10 +0100 Subject: [PATCH 179/298] Allow selecting own repositories (#65695) --- .../components/github/config_flow.py | 50 ++++++++++++++----- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/github/test_config_flow.py | 26 +++++++--- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index f0e27283355..9afbf80297c 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -10,7 +10,6 @@ from aiogithubapi import ( GitHubException, GitHubLoginDeviceModel, GitHubLoginOauthModel, - GitHubRepositoryModel, ) from aiogithubapi.const import OAUTH_USER_LOGIN import voluptuous as vol @@ -34,11 +33,12 @@ from .const import ( ) -async def starred_repositories(hass: HomeAssistant, access_token: str) -> list[str]: - """Return a list of repositories that the user has starred.""" +async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]: + """Return a list of repositories that the user owns or has starred.""" client = GitHubAPI(token=access_token, session=async_get_clientsession(hass)) + repositories = set() - async def _get_starred() -> list[GitHubRepositoryModel] | None: + async def _get_starred_repositories() -> None: response = await client.user.starred(**{"params": {"per_page": 100}}) if not response.is_last_page: results = await asyncio.gather( @@ -54,16 +54,44 @@ async def starred_repositories(hass: HomeAssistant, access_token: str) -> list[s for result in results: response.data.extend(result.data) - return response.data + repositories.update(response.data) + + async def _get_personal_repositories() -> None: + response = await client.user.repos(**{"params": {"per_page": 100}}) + if not response.is_last_page: + results = await asyncio.gather( + *( + client.user.repos( + **{"params": {"per_page": 100, "page": page_number}}, + ) + for page_number in range( + response.next_page_number, response.last_page_number + 1 + ) + ) + ) + for result in results: + response.data.extend(result.data) + + repositories.update(response.data) try: - result = await _get_starred() + await asyncio.gather( + *( + _get_starred_repositories(), + _get_personal_repositories(), + ) + ) + except GitHubException: return DEFAULT_REPOSITORIES - if not result or len(result) == 0: + if len(repositories) == 0: return DEFAULT_REPOSITORIES - return sorted((repo.full_name for repo in result), key=str.casefold) + + return sorted( + (repo.full_name for repo in repositories), + key=str.casefold, + ) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -153,9 +181,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self._login is not None if not user_input: - repositories = await starred_repositories( - self.hass, self._login.access_token - ) + repositories = await get_repositories(self.hass, self._login.access_token) return self.async_show_form( step_id="repositories", data_schema=vol.Schema( @@ -205,7 +231,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): configured_repositories: list[str] = self.config_entry.options[ CONF_REPOSITORIES ] - repositories = await starred_repositories( + repositories = await get_repositories( self.hass, self.config_entry.data[CONF_ACCESS_TOKEN] ) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 474d08c4b0c..8d63b27117e 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,7 +3,7 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==22.1.0" + "aiogithubapi==22.2.0" ], "codeowners": [ "@timmo001", diff --git a/requirements_all.txt b/requirements_all.txt index 4d5cd66a184..e4ed0d86319 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioflo==2021.11.0 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==22.1.0 +aiogithubapi==22.2.0 # homeassistant.components.guardian aioguardian==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6932cdbcd47..cf75245b5db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -125,7 +125,7 @@ aioesphomeapi==10.8.1 aioflo==2021.11.0 # homeassistant.components.github -aiogithubapi==22.1.0 +aiogithubapi==22.2.0 # homeassistant.components.guardian aioguardian==2021.11.0 diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index dad97472620..2bf0fac209f 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiogithubapi import GitHubException from homeassistant import config_entries -from homeassistant.components.github.config_flow import starred_repositories +from homeassistant.components.github.config_flow import get_repositories from homeassistant.components.github.const import ( CONF_ACCESS_TOKEN, CONF_REPOSITORIES, @@ -161,11 +161,19 @@ async def test_starred_pagination_with_paginated_result(hass: HomeAssistant) -> last_page_number=2, data=[MagicMock(full_name="home-assistant/core")], ) - ) + ), + repos=AsyncMock( + return_value=MagicMock( + is_last_page=False, + next_page_number=2, + last_page_number=2, + data=[MagicMock(full_name="awesome/reposiotry")], + ) + ), ) ), ): - repos = await starred_repositories(hass, MOCK_ACCESS_TOKEN) + repos = await get_repositories(hass, MOCK_ACCESS_TOKEN) assert len(repos) == 2 assert repos[-1] == DEFAULT_REPOSITORIES[0] @@ -182,11 +190,17 @@ async def test_starred_pagination_with_no_starred(hass: HomeAssistant) -> None: is_last_page=True, data=[], ) - ) + ), + repos=AsyncMock( + return_value=MagicMock( + is_last_page=True, + data=[], + ) + ), ) ), ): - repos = await starred_repositories(hass, MOCK_ACCESS_TOKEN) + repos = await get_repositories(hass, MOCK_ACCESS_TOKEN) assert len(repos) == 2 assert repos == DEFAULT_REPOSITORIES @@ -200,7 +214,7 @@ async def test_starred_pagination_with_exception(hass: HomeAssistant) -> None: user=MagicMock(starred=AsyncMock(side_effect=GitHubException("Error"))) ), ): - repos = await starred_repositories(hass, MOCK_ACCESS_TOKEN) + repos = await get_repositories(hass, MOCK_ACCESS_TOKEN) assert len(repos) == 2 assert repos == DEFAULT_REPOSITORIES From 609661a862149971490b548afde009b061c9bf75 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 10:43:06 -0800 Subject: [PATCH 180/298] Move scene and button restore to internal hook (#65696) --- homeassistant/components/button/__init__.py | 3 ++- homeassistant/components/scene/__init__.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 2e9a8c05163..d0e27662d41 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -113,8 +113,9 @@ class ButtonEntity(RestoreEntity): self.async_write_ha_state() await self.async_press() - async def async_added_to_hass(self) -> None: + async def async_internal_added_to_hass(self) -> None: """Call when the button is added to hass.""" + await super().async_internal_added_to_hass() state = await self.async_get_last_state() if state is not None and state.state is not None: self.__last_pressed = dt_util.parse_datetime(state.state) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 774aaad0ee4..846c0fbc7c6 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -113,8 +113,9 @@ class Scene(RestoreEntity): self.async_write_ha_state() await self.async_activate(**kwargs) - async def async_added_to_hass(self) -> None: - """Call when the button is added to hass.""" + async def async_internal_added_to_hass(self) -> None: + """Call when the scene is added to hass.""" + await super().async_internal_added_to_hass() state = await self.async_get_last_state() if state is not None and state.state is not None: self.__last_activated = state.state From 5a44f8eaddbee19feea27f603519f6f557811c0a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 10:55:45 -0800 Subject: [PATCH 181/298] Fix passing a string to device registry disabled_by (#65701) --- homeassistant/components/config/device_registry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 50d56915dd4..5e7c2ef1938 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -62,6 +62,9 @@ async def websocket_update_device(hass, connection, msg): msg.pop("type") msg_id = msg.pop("id") + if "disabled_by" in msg: + msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) + entry = registry.async_update_device(**msg) connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry))) From 56d1fc6dad0ace1eabf38a23c2dc1704c321c8ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 11:11:21 -0800 Subject: [PATCH 182/298] Fix tuya diagnostics mutating cached state objects (#65708) --- homeassistant/components/tuya/diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/diagnostics.py b/homeassistant/components/tuya/diagnostics.py index f0e5ed2852f..67bbad0aceb 100644 --- a/homeassistant/components/tuya/diagnostics.py +++ b/homeassistant/components/tuya/diagnostics.py @@ -157,7 +157,7 @@ def _async_device_as_dict(hass: HomeAssistant, device: TuyaDevice) -> dict[str, state = hass.states.get(entity_entry.entity_id) state_dict = None if state: - state_dict = state.as_dict() + state_dict = dict(state.as_dict()) # Redact the `entity_picture` attribute as it contains a token. if "entity_picture" in state_dict["attributes"]: From 1a2e9aaaedab2dfe401c8e5bf9023cc2c31466a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Feb 2022 20:47:01 +0100 Subject: [PATCH 183/298] Depend on diagnostics in the frontend (#65710) --- homeassistant/components/default_config/manifest.json | 1 - homeassistant/components/frontend/manifest.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 94f2aa2b9f6..88f86034aea 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -7,7 +7,6 @@ "cloud", "counter", "dhcp", - "diagnostics", "energy", "frontend", "history", diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 8a9b13648e9..e29b27e9026 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -10,6 +10,7 @@ "auth", "config", "device_automation", + "diagnostics", "http", "lovelace", "onboarding", From 51abdf9c63441dd8e201a877e2f5bf2e6c54d4fc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 12:02:06 -0800 Subject: [PATCH 184/298] Bumped version to 2022.2.2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 17f0d651eab..d05b222fc55 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 8f180fe2a6b..c1de0930cbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.1 +version = 2022.2.2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 5f6214ede7e0c5d5f06535ba067fed60758cb334 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 6 Feb 2022 23:17:10 +0100 Subject: [PATCH 185/298] check wan access type (#65389) --- homeassistant/components/fritz/common.py | 14 ++++++++------ homeassistant/components/fritz/sensor.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 2cd6616f134..eab85ae4087 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -567,11 +567,11 @@ class AvmWrapper(FritzBoxTools): ) return {} - async def async_get_wan_dsl_interface_config(self) -> dict[str, Any]: - """Call WANDSLInterfaceConfig service.""" + async def async_get_wan_link_properties(self) -> dict[str, Any]: + """Call WANCommonInterfaceConfig service.""" return await self.hass.async_add_executor_job( - partial(self.get_wan_dsl_interface_config) + partial(self.get_wan_link_properties) ) async def async_get_wan_link_properties(self) -> dict[str, Any]: @@ -678,10 +678,12 @@ class AvmWrapper(FritzBoxTools): return self._service_call_action("WLANConfiguration", str(index), "GetInfo") - def get_wan_dsl_interface_config(self) -> dict[str, Any]: - """Call WANDSLInterfaceConfig service.""" + def get_wan_link_properties(self) -> dict[str, Any]: + """Call WANCommonInterfaceConfig service.""" - return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo") + return self._service_call_action( + "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" + ) def get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 6155cdc5914..5e4b18eebca 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -277,10 +277,14 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box sensors") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - dsl: bool = False - dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config() - if dslinterface: - dsl = dslinterface["NewEnable"] + link_properties = await avm_wrapper.async_get_wan_link_properties() + dsl: bool = link_properties.get("NewWANAccessType") == "DSL" + + _LOGGER.debug( + "WANAccessType of FritzBox %s is '%s'", + avm_wrapper.host, + link_properties.get("NewWANAccessType"), + ) entities = [ FritzBoxSensor(avm_wrapper, entry.title, description) From d754ea1645ffbc812b59c97fc0157ca8bf7f7e87 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 5 Feb 2022 10:46:52 +0000 Subject: [PATCH 186/298] Fix OVO Energy NoneType error occurring for some users (#65714) --- homeassistant/components/ovo_energy/sensor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index ba332a08a16..8f9a18d1f11 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -121,14 +121,22 @@ async def async_setup_entry( if coordinator.data: if coordinator.data.electricity: for description in SENSOR_TYPES_ELECTRICITY: - if description.key == KEY_LAST_ELECTRICITY_COST: + if ( + description.key == KEY_LAST_ELECTRICITY_COST + and coordinator.data.electricity[-1] is not None + and coordinator.data.electricity[-1].cost is not None + ): description.native_unit_of_measurement = ( coordinator.data.electricity[-1].cost.currency_unit ) entities.append(OVOEnergySensor(coordinator, description, client)) if coordinator.data.gas: for description in SENSOR_TYPES_GAS: - if description.key == KEY_LAST_GAS_COST: + if ( + description.key == KEY_LAST_GAS_COST + and coordinator.data.gas[-1] is not None + and coordinator.data.gas[-1].cost is not None + ): description.native_unit_of_measurement = coordinator.data.gas[ -1 ].cost.currency_unit From eff9690c8a108519ca274d4f975fa63ffb5df300 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Sun, 6 Feb 2022 17:14:44 -0500 Subject: [PATCH 187/298] Fix Amcrest service calls (#65717) Fixes #65522 Fixes #65647 --- homeassistant/components/amcrest/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 6e729a8f1b5..3846f4945a9 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -515,8 +515,8 @@ class AmcrestCam(Camera): max_tries = 3 for tries in range(max_tries, 0, -1): try: - await getattr(self, f"_set_{func}")(value) - new_value = await getattr(self, f"_get_{func}")() + await getattr(self, f"_async_set_{func}")(value) + new_value = await getattr(self, f"_async_get_{func}")() if new_value != value: raise AmcrestCommandFailed except (AmcrestError, AmcrestCommandFailed) as error: From 57526bd21f968ab344f39b0aa8d4edc4ad16e985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Feb 2022 10:59:32 -0600 Subject: [PATCH 188/298] Add coverage for color_rgbww_to_rgb, fix divzero case (#65721) --- homeassistant/util/color.py | 5 ++++- tests/util/test_color.py | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index f055a5f32eb..21c877f9377 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -472,7 +472,10 @@ def color_rgbww_to_rgb( except ZeroDivisionError: ct_ratio = 0.5 color_temp_mired = min_mireds + ct_ratio * mired_range - color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + if color_temp_mired: + color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + else: + color_temp_kelvin = 0 w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) white_level = max(cw, ww) / 255 diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 0b1b8f7d17f..eff71ddef4e 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -405,6 +405,49 @@ def test_color_rgb_to_rgbww(): assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 5) == (103, 69, 0, 255, 255) +def test_color_rgbww_to_rgb(): + """Test color_rgbww_to_rgb conversions.""" + assert color_util.color_rgbww_to_rgb(0, 54, 98, 255, 255, 154, 370) == ( + 255, + 255, + 255, + ) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 154, 370) == ( + 255, + 255, + 255, + ) + assert color_util.color_rgbww_to_rgb(0, 118, 241, 255, 255, 154, 370) == ( + 163, + 204, + 255, + ) + assert color_util.color_rgbww_to_rgb(0, 27, 49, 128, 128, 154, 370) == ( + 128, + 128, + 128, + ) + assert color_util.color_rgbww_to_rgb(0, 14, 25, 64, 64, 154, 370) == (64, 64, 64) + assert color_util.color_rgbww_to_rgb(9, 64, 0, 38, 38, 154, 370) == (32, 64, 16) + assert color_util.color_rgbww_to_rgb(0, 0, 0, 0, 0, 154, 370) == (0, 0, 0) + assert color_util.color_rgbww_to_rgb(103, 69, 0, 255, 255, 153, 370) == ( + 255, + 193, + 112, + ) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 0, 0) == (255, 255, 255) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 0) == ( + 255, + 161, + 128, + ) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 370) == ( + 255, + 245, + 237, + ) + + def test_color_temperature_to_rgbww(): """Test color temp to warm, cold conversion. From 96952359207ee3e62ac0717c8d93878c3b3600a7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 6 Feb 2022 23:13:05 +0100 Subject: [PATCH 189/298] Fix wind speed unit (#65723) --- homeassistant/components/accuweather/weather.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 00726f6db38..4ab9342de62 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -17,7 +17,12 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONF_NAME, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -62,9 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): """Initialize.""" super().__init__(coordinator) self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][ - self._unit_system - ]["Unit"] + wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ + "Unit" + ] + if wind_speed_unit == "mi/h": + self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR + else: + self._attr_wind_speed_unit = wind_speed_unit self._attr_name = name self._attr_unique_id = coordinator.location_key self._attr_temperature_unit = ( From 058420bb2fd427a046f779bc0242d8d6d3c788b6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Feb 2022 00:39:01 -0700 Subject: [PATCH 190/298] Bump simplisafe-python to 2022.02.0 (#65748) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index fa343e7466a..62b5b9aa7b7 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.01.0"], + "requirements": ["simplisafe-python==2022.02.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e4ed0d86319..4fe479bcc64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.01.0 +simplisafe-python==2022.02.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf75245b5db..569162cd357 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.01.0 +simplisafe-python==2022.02.0 # homeassistant.components.slack slackclient==2.5.0 From fc7ea6e1b365c81719620ae3218f66aa9110c46f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 6 Feb 2022 23:15:50 +0100 Subject: [PATCH 191/298] Improve androidtv mac address handling and test coverage (#65749) * Better mac addr handling and improve test coverage * Apply suggested changes * Apply more suggested changes --- .../components/androidtv/__init__.py | 15 ++ .../components/androidtv/config_flow.py | 7 +- .../components/androidtv/media_player.py | 7 +- tests/components/androidtv/patchers.py | 26 ++-- .../components/androidtv/test_config_flow.py | 24 +++- .../components/androidtv/test_media_player.py | 133 ++++++------------ 6 files changed, 94 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 81d4a3f0645..9b968385602 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType @@ -33,16 +34,30 @@ from .const import ( DEVICE_ANDROIDTV, DEVICE_FIRETV, DOMAIN, + PROP_ETHMAC, PROP_SERIALNO, + PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) PLATFORMS = [Platform.MEDIA_PLAYER] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] +_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} + _LOGGER = logging.getLogger(__name__) +def get_androidtv_mac(dev_props): + """Return formatted mac from device properties.""" + for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC): + if if_mac := dev_props.get(prop_mac): + mac = format_mac(if_mac) + if mac not in _INVALID_MACS: + return mac + return None + + def _setup_androidtv(hass, config): """Generate an ADB key (if needed) and load it.""" adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 0ec37fdeb6f..8f0efc34799 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -11,9 +11,8 @@ from homeassistant import config_entries from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import format_mac -from . import async_connect_androidtv +from . import async_connect_androidtv, get_androidtv_mac from .const import ( CONF_ADB_SERVER_IP, CONF_ADB_SERVER_PORT, @@ -132,9 +131,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): PROP_WIFIMAC, dev_prop.get(PROP_WIFIMAC), ) - unique_id = format_mac( - dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "") - ) + unique_id = get_androidtv_mac(dev_prop) await aftv.adb_close() return None, unique_id diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 03b1e679961..1ab592143c6 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -51,12 +51,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, @@ -80,8 +81,6 @@ from .const import ( DEVICE_ANDROIDTV, DEVICE_CLASSES, DOMAIN, - PROP_ETHMAC, - PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) @@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity): self._attr_device_info[ATTR_MANUFACTURER] = manufacturer if sw_version := info.get(ATTR_SW_VERSION): self._attr_device_info[ATTR_SW_VERSION] = sw_version - if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")): + if mac := get_androidtv_mac(info): self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)} self._app_id_to_name = {} diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 4411945c71b..7cc14bbd7b5 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,13 +1,17 @@ """Define patches used for androidtv tests.""" - from unittest.mock import mock_open, patch +from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0 + KEY_PYTHON = "python" KEY_SERVER = "server" ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake" DEVICE_ASYNC_FAKE = "DeviceAsyncFake" +PROPS_DEV_INFO = "fake\nfake\n0123456\nfake" +PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd" + class AdbDeviceTcpAsyncFake: """A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class.""" @@ -100,12 +104,18 @@ def patch_connect(success): } -def patch_shell(response=None, error=False): +def patch_shell(response=None, error=False, mac_eth=False): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" async def shell_success(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful.""" self.shell_cmd = cmd + if cmd == CMD_DEVICE_PROPERTIES: + return PROPS_DEV_INFO + if cmd == CMD_MAC_WLAN0: + return PROPS_DEV_MAC + if cmd == CMD_MAC_ETH0: + return PROPS_DEV_MAC if mac_eth else None return response async def shell_fail_python(self, cmd, *args, **kwargs): @@ -185,15 +195,3 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", side_effect=ZeroDivisionError, ) - -PATCH_DEVICE_PROPERTIES = patch( - "androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties", - return_value={ - "manufacturer": "a", - "model": "b", - "serialno": "c", - "sw_version": "d", - "wifimac": "ab:cd:ef:gh:ij:kl", - "ethmac": None, - }, -) diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index 757be8f6d8d..991d3757749 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -31,6 +31,7 @@ from homeassistant.components.androidtv.const import ( DEFAULT_PORT, DOMAIN, PROP_ETHMAC, + PROP_WIFIMAC, ) from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER @@ -42,6 +43,7 @@ from tests.components.androidtv.patchers import isfile ADBKEY = "adbkey" ETH_MAC = "a1:b1:c1:d1:e1:f1" +WIFI_MAC = "a2:b2:c2:d2:e2:f2" HOST = "127.0.0.1" VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}] @@ -84,18 +86,28 @@ PATCH_SETUP_ENTRY = patch( class MockConfigDevice: """Mock class to emulate Android TV device.""" - def __init__(self, eth_mac=ETH_MAC): + def __init__(self, eth_mac=ETH_MAC, wifi_mac=None): """Initialize a fake device to test config flow.""" self.available = True - self.device_properties = {PROP_ETHMAC: eth_mac} + self.device_properties = {PROP_ETHMAC: eth_mac, PROP_WIFIMAC: wifi_mac} async def adb_close(self): """Fake method to close connection.""" self.available = False -@pytest.mark.parametrize("config", [CONFIG_PYTHON_ADB, CONFIG_ADB_SERVER]) -async def test_user(hass, config): +@pytest.mark.parametrize( + ["config", "eth_mac", "wifi_mac"], + [ + (CONFIG_PYTHON_ADB, ETH_MAC, None), + (CONFIG_ADB_SERVER, ETH_MAC, None), + (CONFIG_PYTHON_ADB, None, WIFI_MAC), + (CONFIG_ADB_SERVER, None, WIFI_MAC), + (CONFIG_PYTHON_ADB, ETH_MAC, WIFI_MAC), + (CONFIG_ADB_SERVER, ETH_MAC, WIFI_MAC), + ], +) +async def test_user(hass, config, eth_mac, wifi_mac): """Test user config.""" flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} @@ -106,7 +118,7 @@ async def test_user(hass, config): # test with all provided with patch( CONNECT_METHOD, - return_value=(MockConfigDevice(), None), + return_value=(MockConfigDevice(eth_mac, wifi_mac), None), ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=config @@ -273,7 +285,7 @@ async def test_invalid_serial(hass): """Test for invalid serialno.""" with patch( CONNECT_METHOD, - return_value=(MockConfigDevice(eth_mac=""), None), + return_value=(MockConfigDevice(eth_mac=None), None), ), PATCH_GET_HOST_IP: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index e97de0fc928..a0bab1736ff 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -142,29 +142,6 @@ def _setup(config): return patch_key, entity_id, config_entry -async def test_setup_with_properties(hass): - """Test that setup succeeds with device properties. - - the response must be a string with the following info separated with line break: - "manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output" - - """ - - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) - config_entry.add_to_hass(hass) - response = "fake\nfake\n0123456\nfake\nether a1:b1:c1:d1:e1:f1 brd\nnone" - - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(response)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - - @pytest.mark.parametrize( "config", [ @@ -190,9 +167,8 @@ async def test_reconnect(hass, caplog, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -259,9 +235,8 @@ async def test_adb_shell_returns_none(hass, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -289,9 +264,8 @@ async def test_setup_with_adbkey(hass): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -324,9 +298,8 @@ async def test_sources(hass, config0): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -404,9 +377,8 @@ async def _test_exclude_sources(hass, config0, expected_sources): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -486,9 +458,8 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -714,9 +685,8 @@ async def test_setup_fail(hass, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -733,9 +703,8 @@ async def test_adb_command(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response @@ -763,9 +732,8 @@ async def test_adb_command_unicode_decode_error(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", @@ -793,9 +761,8 @@ async def test_adb_command_key(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response @@ -823,9 +790,8 @@ async def test_adb_command_get_properties(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict", @@ -853,9 +819,8 @@ async def test_learn_sendevent(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent", @@ -882,9 +847,8 @@ async def test_update_lock_not_acquired(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -918,9 +882,8 @@ async def test_download(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Failed download because path is not whitelisted with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull: @@ -965,9 +928,8 @@ async def test_upload(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Failed upload because path is not whitelisted with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push: @@ -1010,9 +972,8 @@ async def test_androidtv_volume_set(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5 @@ -1038,9 +999,8 @@ async def test_get_image(hass, hass_ws_client): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -1115,9 +1075,8 @@ async def test_services_androidtv(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: await _test_service( @@ -1162,9 +1121,8 @@ async def test_services_firetv(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back") @@ -1179,9 +1137,8 @@ async def test_volume_mute(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True} @@ -1224,9 +1181,8 @@ async def test_connection_closed_on_ha_stop(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close" @@ -1249,9 +1205,8 @@ async def test_exception(hass): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) From bccfe6646ee0aabbab166b44124bb7707971e98e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Feb 2022 00:41:12 -0700 Subject: [PATCH 192/298] Add redacted subscription data to SimpliSafe diagnostics (#65751) --- .../components/simplisafe/__init__.py | 1 + .../components/simplisafe/diagnostics.py | 15 +++ tests/components/simplisafe/conftest.py | 3 +- .../components/simplisafe/test_diagnostics.py | 93 ++++++++++++++++++- 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index a133ec6c2dc..2fd7e10a2f8 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -482,6 +482,7 @@ class SimpliSafe: self._websocket_reconnect_task: asyncio.Task | None = None self.entry = entry self.initial_event_to_use: dict[int, dict[str, Any]] = {} + self.subscription_data: dict[int, Any] = api.subscription_data self.systems: dict[int, SystemType] = {} # This will get filled in by async_init: diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index bc0dddef47c..c7c03467c94 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -11,14 +11,28 @@ from homeassistant.core import HomeAssistant from . import SimpliSafe from .const import DOMAIN +CONF_CREDIT_CARD = "creditCard" +CONF_EXPIRES = "expires" +CONF_LOCATION = "location" +CONF_LOCATION_NAME = "locationName" +CONF_PAYMENT_PROFILE_ID = "paymentProfileId" CONF_SERIAL = "serial" +CONF_SID = "sid" CONF_SYSTEM_ID = "system_id" +CONF_UID = "uid" CONF_WIFI_SSID = "wifi_ssid" TO_REDACT = { CONF_ADDRESS, + CONF_CREDIT_CARD, + CONF_EXPIRES, + CONF_LOCATION, + CONF_LOCATION_NAME, + CONF_PAYMENT_PROFILE_ID, CONF_SERIAL, + CONF_SID, CONF_SYSTEM_ID, + CONF_UID, CONF_WIFI_SSID, } @@ -34,6 +48,7 @@ async def async_get_config_entry_diagnostics( "entry": { "options": dict(entry.options), }, + "subscription_data": simplisafe.subscription_data, "systems": [system.as_dict() for system in simplisafe.systems.values()], }, TO_REDACT, diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index d9e6d46c2eb..d4517717434 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -18,11 +18,12 @@ USER_ID = "12345" @pytest.fixture(name="api") -def api_fixture(system_v3, websocket): +def api_fixture(data_subscription, system_v3, websocket): """Define a fixture for a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), refresh_token=REFRESH_TOKEN, + subscription_data=data_subscription, user_id=USER_ID, websocket=websocket, ) diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index d2c2866bf5b..13d5c778e89 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -7,7 +7,96 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisafe): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": {"options": {}}, + "entry": { + "options": {}, + }, + "subscription_data": { + "system_123": { + "uid": REDACTED, + "sid": REDACTED, + "sStatus": 20, + "activated": 1445034752, + "planSku": "SSEDSM2", + "planName": "Interactive Monitoring", + "price": 24.99, + "currency": "USD", + "country": "US", + "expires": REDACTED, + "canceled": 0, + "extraTime": 0, + "creditCard": REDACTED, + "time": 2628000, + "paymentProfileId": REDACTED, + "features": { + "monitoring": True, + "alerts": True, + "online": True, + "hazard": True, + "video": True, + "cameras": 10, + "dispatch": True, + "proInstall": False, + "discount": 0, + "vipCS": False, + "medical": True, + "careVisit": False, + "storageDays": 30, + }, + "status": { + "hasBaseStation": True, + "isActive": True, + "monitoring": "Active", + }, + "subscriptionFeatures": { + "monitoredSensorsTypes": [ + "Entry", + "Motion", + "GlassBreak", + "Smoke", + "CO", + "Freeze", + "Water", + ], + "monitoredPanicConditions": ["Fire", "Medical", "Duress"], + "dispatchTypes": ["Police", "Fire", "Medical", "Guard"], + "remoteControl": [ + "ArmDisarm", + "LockUnlock", + "ViewSettings", + "ConfigureSettings", + ], + "cameraFeatures": { + "liveView": True, + "maxRecordingCameras": 10, + "recordingStorageDays": 30, + "videoVerification": True, + }, + "support": { + "level": "Basic", + "annualVisit": False, + "professionalInstall": False, + }, + "cellCommunicationBackup": True, + "alertChannels": ["Push", "SMS", "Email"], + "alertTypes": ["Alarm", "Error", "Activity", "Camera"], + "alarmModes": ["Alarm", "SecretAlert", "Disabled"], + "supportedIntegrations": [ + "GoogleAssistant", + "AmazonAlexa", + "AugustLock", + ], + "timeline": {}, + }, + "dispatcher": "cops", + "dcid": 0, + "location": REDACTED, + "pinUnlocked": True, + "billDate": 1602887552, + "billInterval": 2628000, + "pinUnlockedBy": "pin", + "autoActivation": None, + } + }, "systems": [ { "address": REDACTED, @@ -183,7 +272,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa "shutter_open_when_off": False, "status": "online", "subscription_enabled": True, - }, + } ], "chime_volume": 2, "entry_delay_away": 30, From 5786f68bb71754356a37481ac178f83fdd2a00d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Feb 2022 10:59:22 -0600 Subject: [PATCH 193/298] Prevent multiple dhcp flows from being started for the same device/domain (#65753) --- homeassistant/components/dhcp/__init__.py | 6 +++++ tests/components/dhcp/test_init.py | 27 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 4310f8f2caf..dd247c4cab9 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -179,6 +179,7 @@ class WatcherBase: lowercase_hostname, ) + matched_domains = set() for entry in self._integration_matchers: if MAC_ADDRESS in entry and not fnmatch.fnmatch( uppercase_mac, entry[MAC_ADDRESS] @@ -191,6 +192,11 @@ class WatcherBase: continue _LOGGER.debug("Matched %s against %s", data, entry) + if entry["domain"] in matched_domains: + # Only match once per domain + continue + + matched_domains.add(entry["domain"]) discovery_flow.async_create_flow( self.hass, entry["domain"], diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index fb3387aeab6..0956230d787 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -255,6 +255,33 @@ async def test_dhcp_match_macaddress(hass): ) +async def test_dhcp_multiple_match_only_one_flow(hass): + """Test matching the domain multiple times only generates one flow.""" + integration_matchers = [ + {"domain": "mock-domain", "macaddress": "B8B7F1*"}, + {"domain": "mock-domain", "hostname": "connect"}, + ] + + packet = Ether(RAW_DHCP_REQUEST) + + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + await async_handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) + + async def test_dhcp_match_macaddress_without_hostname(hass): """Test matching based on macaddress only.""" integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}] From 7a7f9deb89034f3ab475ddee4894ebded523236f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Feb 2022 14:19:24 +0100 Subject: [PATCH 194/298] Update Pillow to 9.0.1 (#65779) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 3957b257364..86ad7ae4a90 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.0.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.0.1"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 5f624ea8e1c..2363b124e43 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.0.0"], + "requirements": ["pillow==9.0.1"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 4f19e6afae2..d1be59ebc87 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.0.0"], + "requirements": ["pillow==9.0.1"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 63eca334d7b..37697f2af83 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.0.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.0.1", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated" } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index a49a471038c..db8e57673b1 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.0.0"], + "requirements": ["pillow==9.0.1"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index def1359b1ee..8edef306d8d 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.0.0", "simplehound==0.3"], + "requirements": ["pillow==9.0.1", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 26b7421ef44..b9b0ff6b5c5 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.21.4", - "pillow==9.0.0" + "pillow==9.0.1" ], "codeowners": [], "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f0b9516ead..5cded6a179d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 -pillow==9.0.0 +pillow==9.0.1 pip>=8.0.3,<20.3 pyserial==3.5 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4fe479bcc64..84311dca3f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1249,7 +1249,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.0.0 +pillow==9.0.1 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 569162cd357..760dc13b252 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -771,7 +771,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.0.0 +pillow==9.0.1 # homeassistant.components.plex plexapi==4.9.1 From 4ba494f5cd5a8613d1a5e8168fad2864d4829001 Mon Sep 17 00:00:00 2001 From: Ferdinand <96470489+Smitplaza@users.noreply.github.com> Date: Sat, 5 Feb 2022 17:12:17 +0100 Subject: [PATCH 195/298] Fix the restart when the saj device is down (#65796) --- homeassistant/components/saj/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 670455d7354..2fb3729d0a8 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_TYPE, CONF_USERNAME, ENERGY_KILO_WATT_HOUR, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, MASS_KILOGRAMS, POWER_WATT, @@ -33,6 +32,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -131,17 +131,19 @@ async def async_setup_platform( return values + @callback def start_update_interval(event): """Start the update interval scheduling.""" nonlocal remove_interval_update remove_interval_update = async_track_time_interval_backoff(hass, async_saj) + @callback def stop_update_interval(event): """Properly cancel the scheduled update.""" remove_interval_update() # pylint: disable=not-callable - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) + async_at_start(hass, start_update_interval) @callback From a4d59aa599d44e9ec2fb1d597b2971c64b36268f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 5 Feb 2022 17:44:05 +0200 Subject: [PATCH 196/298] Bump aioshelly to 1.0.9 (#65803) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 568f2b878ae..1d269705652 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.8"], + "requirements": ["aioshelly==1.0.9"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 84311dca3f1..8ce4927f3af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -254,7 +254,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.8 +aioshelly==1.0.9 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 760dc13b252..fb05ad28c59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.8 +aioshelly==1.0.9 # homeassistant.components.steamist aiosteamist==0.3.1 From 619a52a387210a67fa1f0d9136e681feffd05a84 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 6 Feb 2022 14:14:25 -0800 Subject: [PATCH 197/298] Fix legacy nest diagnostics to return empty rather than fail (#65824) Fix legacy nest diangostics to return gracefully, rather than a TypError by checking explicitiy for SDM in the config entry. Update diagnostics to use the common nest test fixture, and extend with support for the legacy nest config. Use the sdm test fixture in the existing legacy tests so they all share the same config files. --- homeassistant/components/nest/diagnostics.py | 5 +- tests/components/nest/common.py | 18 ++++ .../nest/test_config_flow_legacy.py | 6 +- tests/components/nest/test_diagnostics.py | 85 ++++++++++--------- tests/components/nest/test_init_legacy.py | 31 ++----- 5 files changed, 78 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index 0b6cfff6bae..859aa834581 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -12,7 +12,7 @@ from google_nest_sdm.exceptions import ApiException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN REDACT_DEVICE_TRAITS = {InfoTrait.NAME} @@ -21,6 +21,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict: """Return diagnostics for a config entry.""" + if DATA_SDM not in config_entry.data: + return {} + if DATA_SUBSCRIBER not in hass.data[DOMAIN]: return {"error": "No subscriber configured"} diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index ca761e8987f..e80ca84d58f 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -102,6 +102,24 @@ TEST_CONFIG_HYBRID = NestTestConfig( }, ) +TEST_CONFIG_LEGACY = NestTestConfig( + config={ + "nest": { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + }, + }, + config_entry_data={ + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, + }, +) + class FakeSubscriber(GoogleNestSubscriber): """Fake subscriber that supplies a FakeDeviceManager.""" diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index d21920b9e6f..843c9b582ae 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -6,9 +6,11 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component +from .common import TEST_CONFIG_LEGACY + from tests.common import MockConfigEntry -CONFIG = {DOMAIN: {"client_id": "bla", "client_secret": "bla"}} +CONFIG = TEST_CONFIG_LEGACY.config async def test_abort_if_no_implementation_registered(hass): @@ -59,7 +61,7 @@ async def test_full_flow_implementation(hass): assert ( result["description_placeholders"] .get("url") - .startswith("https://home.nest.com/login/oauth2?client_id=bla") + .startswith("https://home.nest.com/login/oauth2?client_id=some-client-id") ) def mock_login(auth): diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index b603019da81..cf6c9c5b20f 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -2,54 +2,45 @@ from unittest.mock import patch -from google_nest_sdm.device import Device from google_nest_sdm.exceptions import SubscriberException +import pytest -from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.setup import async_setup_component -from .common import CONFIG, async_setup_sdm_platform, create_config_entry +from .common import TEST_CONFIG_LEGACY from tests.components.diagnostics import get_diagnostics_for_config_entry -THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT" - -async def test_entry_diagnostics(hass, hass_client): +async def test_entry_diagnostics( + hass, hass_client, create_device, setup_platform, config_entry +): """Test config entry diagnostics.""" - devices = { - "some-device-id": Device.MakeDevice( - { - "name": "enterprises/project-id/devices/device-id", - "type": "sdm.devices.types.THERMOSTAT", - "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", - "traits": { - "sdm.devices.traits.Info": { - "customName": "My Sensor", - }, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1, - }, - "sdm.devices.traits.Humidity": { - "ambientHumidityPercent": 35.0, - }, + create_device.create( + raw_data={ + "name": "enterprises/project-id/devices/device-id", + "type": "sdm.devices.types.THERMOSTAT", + "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Sensor", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, }, - "parentRelations": [ - { - "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", - "displayName": "Lobby", - } - ], }, - auth=None, - ) - } - assert await async_setup_sdm_platform(hass, platform=None, devices=devices) - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - config_entry = entries[0] + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", + "displayName": "Lobby", + } + ], + } + ) + await setup_platform() assert config_entry.state is ConfigEntryState.LOADED # Test that only non identifiable device information is returned @@ -76,20 +67,32 @@ async def test_entry_diagnostics(hass, hass_client): } -async def test_setup_susbcriber_failure(hass, hass_client): +async def test_setup_susbcriber_failure( + hass, hass_client, config_entry, setup_base_platform +): """Test configuration error.""" - config_entry = create_config_entry() - config_entry.add_to_hass(hass) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ), patch( "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", side_effect=SubscriberException(), ): - assert await async_setup_component(hass, DOMAIN, CONFIG) + await setup_base_platform() assert config_entry.state is ConfigEntryState.SETUP_RETRY assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "error": "No subscriber configured" } + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY]) +async def test_legacy_config_entry_diagnostics( + hass, hass_client, config_entry, setup_base_platform +): + """Test config entry diagnostics for legacy integration doesn't fail.""" + + with patch("homeassistant.components.nest.legacy.Nest"): + await setup_base_platform() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index 3a78877a235..cbf1bfe2d48 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -1,30 +1,18 @@ """Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" -import time from unittest.mock import MagicMock, PropertyMock, patch -from homeassistant.setup import async_setup_component +import pytest -from tests.common import MockConfigEntry +from .common import TEST_CONFIG_LEGACY DOMAIN = "nest" -CONFIG = { - "nest": { - "client_id": "some-client-id", - "client_secret": "some-client-secret", - }, -} -CONFIG_ENTRY_DATA = { - "auth_implementation": "local", - "tokens": { - "expires_at": time.time() + 86400, - "access_token": { - "token": "some-token", - }, - }, -} +@pytest.fixture +def nest_test_config(): + """Fixture to specify the overall test fixture configuration.""" + return TEST_CONFIG_LEGACY def make_thermostat(): @@ -45,7 +33,7 @@ def make_thermostat(): return device -async def test_thermostat(hass): +async def test_thermostat(hass, setup_base_platform): """Test simple initialization for thermostat entities.""" thermostat = make_thermostat() @@ -58,8 +46,6 @@ async def test_thermostat(hass): nest = MagicMock() type(nest).structures = PropertyMock(return_value=[structure]) - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", ["humidity", "temperature"], @@ -67,8 +53,7 @@ async def test_thermostat(hass): "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", {"fan": None}, ): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() + await setup_base_platform() climate = hass.states.get("climate.my_thermostat") assert climate is not None From 8e6bd840a4df00fd490bac846f4d3a0aed0f67ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Feb 2022 22:49:37 -0600 Subject: [PATCH 198/298] Fix flash at turn on with newer 0x04 Magic Home models (#65836) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index e10bf72b8c3..4e6cf97fa31 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.20"], + "requirements": ["flux_led==0.28.21"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 8ce4927f3af..5f690e159b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.20 +flux_led==0.28.21 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb05ad28c59..827e2795999 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.20 +flux_led==0.28.21 # homeassistant.components.homekit fnvhash==0.1.0 From fdfffcb73eb36b25934a97c43c194fed901f6600 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 6 Feb 2022 11:37:23 -0600 Subject: [PATCH 199/298] Fix Spotify, Tidal, Apple Music playback on Sonos groups (#65838) --- homeassistant/components/sonos/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index d490120faf8..41453117c13 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -558,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): plex_plugin.play_now(media) return - share_link = self.speaker.share_link + share_link = self.coordinator.share_link if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE): share_link.add_share_link_to_queue(media_id) From 779171160390f4f3ef07a1bab1e68d39b6ff6e95 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 6 Feb 2022 01:32:04 -0700 Subject: [PATCH 200/298] feat: bumped version (#65863) --- homeassistant/components/intellifire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 42edf00ad25..965bb6f32db 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,7 +3,7 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==0.5"], + "requirements": ["intellifire4py==0.6"], "dependencies": [], "codeowners": ["@jeeftor"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 5f690e159b1..f51b7a08619 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -914,7 +914,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.5 +intellifire4py==0.6 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 827e2795999..c28427ac595 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -586,7 +586,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.5 +intellifire4py==0.6 # homeassistant.components.iotawatt iotawattpy==0.1.0 From b9d346baed5ae87c2d10695930dbf81c5a9a2053 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Feb 2022 16:12:30 -0600 Subject: [PATCH 201/298] Fix loss of ability to control white channel in HomeKit on RGB&W lights (#65864) * Fix loss of ability to control white channel in HomeKit on RGB&W lights - Fix white channel missing from RGB/W lights - Fix temp missing from RGB/CW lights - Fixes #65529 * cover the missing case * bright fix * force brightness notify on color mode change as well --- .../components/homekit/type_lights.py | 152 +++--- tests/components/homekit/test_type_lights.py | 462 ++++++++++++++++-- 2 files changed, 522 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index cdff3105ec3..081f6f1bdd4 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,4 +1,6 @@ """Class to hold all light accessories.""" +from __future__ import annotations + import logging import math @@ -12,12 +14,13 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, - ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, DOMAIN, brightness_supported, color_supported, @@ -32,9 +35,9 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.color import ( - color_hsv_to_RGB, color_temperature_mired_to_kelvin, color_temperature_to_hs, + color_temperature_to_rgbww, ) from .accessories import TYPES, HomeAccessory @@ -51,12 +54,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -RGB_COLOR = "rgb_color" CHANGE_COALESCE_TIME_WINDOW = 0.01 +DEFAULT_MIN_MIREDS = 153 +DEFAULT_MAX_MIREDS = 500 -COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} +COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE} @TYPES.register("Light") @@ -79,8 +83,12 @@ class Light(HomeAccessory): self.color_modes = color_modes = ( attributes.get(ATTR_SUPPORTED_COLOR_MODES) or [] ) + self._previous_color_mode = attributes.get(ATTR_COLOR_MODE) self.color_supported = color_supported(color_modes) self.color_temp_supported = color_temp_supported(color_modes) + self.rgbw_supported = COLOR_MODE_RGBW in color_modes + self.rgbww_supported = COLOR_MODE_RGBWW in color_modes + self.white_supported = COLOR_MODE_WHITE in color_modes self.brightness_supported = brightness_supported(color_modes) if self.brightness_supported: @@ -89,7 +97,9 @@ class Light(HomeAccessory): if self.color_supported: self.chars.extend([CHAR_HUE, CHAR_SATURATION]) - if self.color_temp_supported: + if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection( + self.color_modes + ): self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) @@ -101,13 +111,22 @@ class Light(HomeAccessory): # to set to the correct initial value. self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) - if self.color_temp_supported: - min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) - max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) + if CHAR_COLOR_TEMPERATURE in self.chars: + self.min_mireds = math.floor( + attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS) + ) + self.max_mireds = math.ceil( + attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS) + ) + if not self.color_temp_supported and not self.rgbww_supported: + self.max_mireds = self.min_mireds self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, - value=min_mireds, - properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, + value=self.min_mireds, + properties={ + PROP_MIN_VALUE: self.min_mireds, + PROP_MAX_VALUE: self.max_mireds, + }, ) if self.color_supported: @@ -165,33 +184,32 @@ class Light(HomeAccessory): ) return + # Handle white channels if CHAR_COLOR_TEMPERATURE in char_values: - params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] - events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") + temp = char_values[CHAR_COLOR_TEMPERATURE] + events.append(f"color temperature at {temp}") + bright_val = round( + ((brightness_pct or self.char_brightness.value) * 255) / 100 + ) + if self.color_temp_supported: + params[ATTR_COLOR_TEMP] = temp + elif self.rgbww_supported: + params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww( + temp, bright_val, self.min_mireds, self.max_mireds + ) + elif self.rgbw_supported: + params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val) + elif self.white_supported: + params[ATTR_WHITE] = bright_val - elif ( - CHAR_HUE in char_values - or CHAR_SATURATION in char_values - # If we are adjusting brightness we need to send the full RGBW/RGBWW values - # since HomeKit does not support RGBW/RGBWW - or brightness_pct - and COLOR_MODES_WITH_WHITES.intersection(self.color_modes) - ): + elif CHAR_HUE in char_values or CHAR_SATURATION in char_values: hue_sat = ( char_values.get(CHAR_HUE, self.char_hue.value), char_values.get(CHAR_SATURATION, self.char_saturation.value), ) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat) events.append(f"set color at {hue_sat}") - # HomeKit doesn't support RGBW/RGBWW so we need to remove any white values - if COLOR_MODE_RGBWW in self.color_modes: - val = brightness_pct or self.char_brightness.value - params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0) - elif COLOR_MODE_RGBW in self.color_modes: - val = brightness_pct or self.char_brightness.value - params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0) - else: - params[ATTR_HS_COLOR] = hue_sat + params[ATTR_HS_COLOR] = hue_sat if ( brightness_pct @@ -200,6 +218,9 @@ class Light(HomeAccessory): ): params[ATTR_BRIGHTNESS_PCT] = brightness_pct + _LOGGER.debug( + "Calling light service with params: %s -> %s", char_values, params + ) self.async_call_service(DOMAIN, service, params, ", ".join(events)) @callback @@ -210,52 +231,59 @@ class Light(HomeAccessory): attributes = new_state.attributes color_mode = attributes.get(ATTR_COLOR_MODE) self.char_on.set_value(int(state == STATE_ON)) + color_mode_changed = self._previous_color_mode != color_mode + self._previous_color_mode = color_mode # Handle Brightness - if self.brightness_supported: - if ( - color_mode - and COLOR_MODES_WITH_WHITES.intersection({color_mode}) - and (rgb_color := attributes.get(ATTR_RGB_COLOR)) - ): - # HomeKit doesn't support RGBW/RGBWW so we need to - # give it the color brightness only - brightness = max(rgb_color) - else: - brightness = attributes.get(ATTR_BRIGHTNESS) - if isinstance(brightness, (int, float)): - brightness = round(brightness / 255 * 100, 0) - # The homeassistant component might report its brightness as 0 but is - # not off. But 0 is a special value in homekit. When you turn on a - # homekit accessory it will try to restore the last brightness state - # which will be the last value saved by char_brightness.set_value. - # But if it is set to 0, HomeKit will update the brightness to 100 as - # it thinks 0 is off. - # - # Therefore, if the the brightness is 0 and the device is still on, - # the brightness is mapped to 1 otherwise the update is ignored in - # order to avoid this incorrect behavior. - if brightness == 0 and state == STATE_ON: - brightness = 1 - self.char_brightness.set_value(brightness) + if ( + self.brightness_supported + and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None + and isinstance(brightness, (int, float)) + ): + brightness = round(brightness / 255 * 100, 0) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brightness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0 and state == STATE_ON: + brightness = 1 + self.char_brightness.set_value(brightness) + if color_mode_changed: + self.char_brightness.notify() # Handle Color - color must always be set before color temperature # or the iOS UI will not display it correctly. if self.color_supported: - if ATTR_COLOR_TEMP in attributes: + if color_temp := attributes.get(ATTR_COLOR_TEMP): hue, saturation = color_temperature_to_hs( - color_temperature_mired_to_kelvin( - new_state.attributes[ATTR_COLOR_TEMP] - ) + color_temperature_mired_to_kelvin(color_temp) ) + elif color_mode == COLOR_MODE_WHITE: + hue, saturation = 0, 0 else: hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): self.char_hue.set_value(round(hue, 0)) self.char_saturation.set_value(round(saturation, 0)) + if color_mode_changed: + # If the color temp changed, be sure to force the color to update + self.char_hue.notify() + self.char_saturation.notify() - # Handle color temperature - if self.color_temp_supported: - color_temp = attributes.get(ATTR_COLOR_TEMP) + # Handle white channels + if CHAR_COLOR_TEMPERATURE in self.chars: + color_temp = None + if self.color_temp_supported: + color_temp = attributes.get(ATTR_COLOR_TEMP) + elif color_mode == COLOR_MODE_WHITE: + color_temp = self.min_mireds if isinstance(color_temp, (int, float)): self.char_color_temp.set_value(round(color_temp, 0)) + if color_mode_changed: + self.char_color_temp.notify() diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 8e7b60b0a47..6835629be37 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -5,7 +5,11 @@ from datetime import timedelta from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest -from homeassistant.components.homekit.const import ATTR_VALUE +from homeassistant.components.homekit.const import ( + ATTR_VALUE, + PROP_MAX_VALUE, + PROP_MIN_VALUE, +) from homeassistant.components.homekit.type_lights import ( CHANGE_COALESCE_TIME_WINDOW, Light, @@ -22,9 +26,12 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE, COLOR_MODE_COLOR_TEMP, + COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, DOMAIN, ) from homeassistant.const import ( @@ -573,7 +580,7 @@ async def test_light_restore(hass, hk_driver, events): @pytest.mark.parametrize( - "supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", + "supported_color_modes, state_props, turn_on_props_with_brightness", [ [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], @@ -584,8 +591,7 @@ async def test_light_restore(hass, hk_driver, events): ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBW, }, - {ATTR_RGBW_COLOR: (31, 127, 71, 0)}, - {ATTR_RGBW_COLOR: (15, 63, 35, 0)}, + {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, ], [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], @@ -596,21 +602,19 @@ async def test_light_restore(hass, hk_driver, events): ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBWW, }, - {ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)}, - {ATTR_RGBWW_COLOR: (15, 63, 35, 0, 0)}, + {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, ], ], ) -async def test_light_rgb_with_white( +async def test_light_rgb_with_color_temp( hass, hk_driver, events, supported_color_modes, state_props, - turn_on_props, turn_on_props_with_brightness, ): - """Test lights with RGBW/RGBWW.""" + """Test lights with RGBW/RGBWW with color temp support.""" entity_id = "light.demo" hass.states.async_set( @@ -629,7 +633,7 @@ async def test_light_rgb_with_white( await hass.async_block_till_done() assert acc.char_hue.value == 23 assert acc.char_saturation.value == 100 - assert acc.char_brightness.value == 50 + assert acc.char_brightness.value == 100 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -658,11 +662,10 @@ async def test_light_rgb_with_white( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - for k, v in turn_on_props.items(): - assert call_turn_on[-1].data[k] == v + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" - assert acc.char_brightness.value == 50 + assert acc.char_brightness.value == 100 hk_driver.set_characteristics( { @@ -697,7 +700,204 @@ async def test_light_rgb_with_white( @pytest.mark.parametrize( - "supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", + "supported_color_modes, state_props, turn_on_props_with_brightness", + [ + [ + [COLOR_MODE_RGBW], + { + ATTR_RGBW_COLOR: (128, 50, 0, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, + }, + {ATTR_RGBW_COLOR: (0, 0, 0, 191)}, + ], + [ + [COLOR_MODE_RGBWW], + { + ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + }, + {ATTR_RGBWW_COLOR: (0, 0, 0, 165, 26)}, + ], + ], +) +async def test_light_rgbwx_with_color_temp_and_brightness( + hass, + hk_driver, + events, + supported_color_modes, + state_props, + turn_on_props_with_brightness, +): + """Test lights with RGBW/RGBWW with color temp support and setting brightness.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + {ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, **state_props}, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 200, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + for k, v in turn_on_props_with_brightness.items(): + assert call_turn_on[-1].data[k] == v + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "brightness at 75%, color temperature at 200" + assert acc.char_brightness.value == 75 + + +async def test_light_rgb_or_w_lights( + hass, + hk_driver, + events, +): + """Test lights with RGB or W lights.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE], + ATTR_RGBW_COLOR: (128, 50, 0, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGB, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + assert acc.char_color_temp.value == 153 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert acc.char_brightness.value == 100 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: acc.min_mireds, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 25, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_WHITE] == round(25 * 255 / 100) + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "brightness at 25%, color temperature at 153" + assert acc.char_brightness.value == 25 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE], + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_WHITE, + }, + ) + await hass.async_block_till_done() + assert acc.char_hue.value == 0 + assert acc.char_saturation.value == 0 + assert acc.char_brightness.value == 100 + assert acc.char_color_temp.value == 153 + + +@pytest.mark.parametrize( + "supported_color_modes, state_props", [ [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], @@ -708,8 +908,6 @@ async def test_light_rgb_with_white( ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBW, }, - {ATTR_RGBW_COLOR: (31, 127, 71, 0)}, - {ATTR_COLOR_TEMP: 2700}, ], [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], @@ -720,8 +918,6 @@ async def test_light_rgb_with_white( ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBWW, }, - {ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)}, - {ATTR_COLOR_TEMP: 2700}, ], ], ) @@ -731,8 +927,6 @@ async def test_light_rgb_with_white_switch_to_temp( events, supported_color_modes, state_props, - turn_on_props, - turn_on_props_with_brightness, ): """Test lights with RGBW/RGBWW that preserves brightness when switching to color temp.""" entity_id = "light.demo" @@ -753,7 +947,7 @@ async def test_light_rgb_with_white_switch_to_temp( await hass.async_block_till_done() assert acc.char_hue.value == 23 assert acc.char_saturation.value == 100 - assert acc.char_brightness.value == 50 + assert acc.char_brightness.value == 100 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -782,19 +976,17 @@ async def test_light_rgb_with_white_switch_to_temp( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - for k, v in turn_on_props.items(): - assert call_turn_on[-1].data[k] == v + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" - assert acc.char_brightness.value == 50 - + assert acc.char_brightness.value == 100 hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_color_temp_iid, - HAP_REPR_VALUE: 2700, + HAP_REPR_VALUE: 500, }, ] }, @@ -803,11 +995,221 @@ async def test_light_rgb_with_white_switch_to_temp( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - for k, v in turn_on_props_with_brightness.items(): - assert call_turn_on[-1].data[k] == v + assert call_turn_on[-1].data[ATTR_COLOR_TEMP] == 500 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == "color temperature at 2700" - assert acc.char_brightness.value == 50 + assert events[-1].data[ATTR_VALUE] == "color temperature at 500" + assert acc.char_brightness.value == 100 + + +async def test_light_rgbww_with_color_temp_conversion( + hass, + hk_driver, + events, +): + """Test lights with RGBWW convert color temp as expected.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW], + ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert acc.char_brightness.value == 100 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 200, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_RGBWW_COLOR] == (0, 0, 0, 220, 35) + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "color temperature at 200" + assert acc.char_brightness.value == 100 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW], + ATTR_RGBWW_COLOR: (0, 0, 0, 128, 255), + ATTR_RGB_COLOR: (255, 163, 79), + ATTR_HS_COLOR: (28.636, 69.02), + ATTR_BRIGHTNESS: 180, + ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + }, + ) + await hass.async_block_till_done() + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 100, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_BRIGHTNESS_PCT] == 100 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == "brightness at 100%" + assert acc.char_brightness.value == 100 + + +async def test_light_rgbw_with_color_temp_conversion( + hass, + hk_driver, + events, +): + """Test lights with RGBW convert color temp as expected.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + assert ( + acc.char_color_temp.properties[PROP_MIN_VALUE] + == acc.char_color_temp.properties[PROP_MAX_VALUE] + ) + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert acc.char_brightness.value == 100 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 153, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_RGBW_COLOR] == (0, 0, 0, 255) + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "color temperature at 153" + assert acc.char_brightness.value == 100 async def test_light_set_brightness_and_color(hass, hk_driver, events): From 0dbe9b7cf4afaa6a2044794842d61e52c26b4ea8 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 6 Feb 2022 17:48:56 +0100 Subject: [PATCH 202/298] Update xknx to 0.19.2 - fix TCP tunnelling (#65920) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 9889f39ab35..663c0e5839a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.19.1" + "xknx==0.19.2" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index f51b7a08619..d0b39f593c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2496,7 +2496,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.19.1 +xknx==0.19.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c28427ac595..a87796f665c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1527,7 +1527,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.19.1 +xknx==0.19.2 # homeassistant.components.bluesound # homeassistant.components.fritz From ad3b2f02b4580e6a1fbd6b803312927c5fba4999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 6 Feb 2022 20:23:31 +0100 Subject: [PATCH 203/298] disabled_by can be None when updating devices (#65934) --- homeassistant/components/config/device_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 5e7c2ef1938..686fffec252 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -62,7 +62,7 @@ async def websocket_update_device(hass, connection, msg): msg.pop("type") msg_id = msg.pop("id") - if "disabled_by" in msg: + if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) entry = registry.async_update_device(**msg) From aa9965675d222f5a0da8c3c45534669c1e57a7b8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 6 Feb 2022 23:02:31 +0100 Subject: [PATCH 204/298] Improve device shutdown and unload of Synology DSM integration (#65936) * ignore errors during unload/logout * automatic host update is an info, nut debug --- homeassistant/components/synology_dsm/common.py | 7 ++++++- homeassistant/components/synology_dsm/config_flow.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 273c9cc6a42..54a0735186f 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -16,6 +16,7 @@ from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, + SynologyDSMException, SynologyDSMLoginFailedException, SynologyDSMRequestException, ) @@ -237,7 +238,11 @@ class SynoApi: async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" - await self._syno_api_executer(self.dsm.logout) + try: + await self._syno_api_executer(self.dsm.logout) + except SynologyDSMException: + # ignore API errors during logout + pass async def async_update(self, now: timedelta | None = None) -> None: """Update function for updating API information.""" diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 91ad49c5f84..256ad5eef8e 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -267,7 +267,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): and existing_entry.data[CONF_HOST] != parsed_url.hostname and not fqdn_with_ssl_verification ): - _LOGGER.debug( + _LOGGER.info( "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", existing_entry.data[CONF_HOST], parsed_url.hostname, From e90a6bbe1cc423543166614f43cbfebecb98634a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Feb 2022 16:02:07 -0600 Subject: [PATCH 205/298] Add diagnostics support to HomeKit (#65942) * Add diagnostics support to HomeKit * remove debug --- .../components/homekit/diagnostics.py | 44 +++++++ tests/components/homekit/test_diagnostics.py | 119 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 homeassistant/components/homekit/diagnostics.py create mode 100644 tests/components/homekit/test_diagnostics.py diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py new file mode 100644 index 00000000000..2a54c1ef543 --- /dev/null +++ b/homeassistant/components/homekit/diagnostics.py @@ -0,0 +1,44 @@ +"""Diagnostics support for HomeKit.""" +from __future__ import annotations + +from typing import Any + +from pyhap.accessory_driver import AccessoryDriver +from pyhap.state import State + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import HomeKit +from .const import DOMAIN, HOMEKIT + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] + driver: AccessoryDriver = homekit.driver + data: dict[str, Any] = { + "status": homekit.status, + "config-entry": { + "title": entry.title, + "version": entry.version, + "data": dict(entry.data), + "options": dict(entry.options), + }, + } + if not driver: + return data + data.update(driver.get_accessories()) + state: State = driver.state + data.update( + { + "client_properties": { + str(client): props for client, props in state.client_properties.items() + }, + "config_version": state.config_version, + "pairing_id": state.mac, + } + ) + return data diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py new file mode 100644 index 00000000000..e3a85b85972 --- /dev/null +++ b/tests/components/homekit/test_diagnostics.py @@ -0,0 +1,119 @@ +"""Test homekit diagnostics.""" +from unittest.mock import ANY, patch + +from homeassistant.components.homekit.const import DOMAIN +from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED + +from .util import async_init_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_config_entry_not_running( + hass, hass_client, hk_driver, mock_async_zeroconf +): + """Test generating diagnostics for a config entry.""" + entry = await async_init_integration(hass) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "config-entry": { + "data": {"name": "mock_name", "port": 12345}, + "options": {}, + "title": "Mock Title", + "version": 1, + }, + "status": 0, + } + + +async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zeroconf): + """Test generating diagnostics for a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "accessories": [ + { + "aid": 1, + "services": [ + { + "characteristics": [ + {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"}, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Home Assistant", + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "Bridge", + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "mock_name", + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": "homekit.bridge", + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": ANY, + }, + ], + "iid": 1, + "type": "3E", + }, + { + "characteristics": [ + { + "format": "string", + "iid": 9, + "perms": ["pr", "ev"], + "type": "37", + "value": "01.01.00", + } + ], + "iid": 8, + "type": "A2", + }, + ], + } + ], + "client_properties": {}, + "config-entry": { + "data": {"name": "mock_name", "port": 12345}, + "options": {}, + "title": "Mock Title", + "version": 1, + }, + "config_version": 2, + "pairing_id": ANY, + "status": 1, + } + + with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( + "homeassistant.components.homekit.HomeKit.async_stop" + ), patch("homeassistant.components.homekit.async_port_is_available"): + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 9b471ab653bfc6970b22077028b969678f8c55a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Feb 2022 14:23:08 -0800 Subject: [PATCH 206/298] Bumped version to 2022.2.3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d05b222fc55..5c2939e0b1f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index c1de0930cbc..8d9f8974eb7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.2 +version = 2022.2.3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 1338b347b5bbc418a1681f1b00004f0a0eab2f03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Feb 2022 14:33:07 -0800 Subject: [PATCH 207/298] Remove duplicate methods --- homeassistant/components/fritz/common.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index eab85ae4087..078bb4b4225 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -574,13 +574,6 @@ class AvmWrapper(FritzBoxTools): partial(self.get_wan_link_properties) ) - async def async_get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return await self.hass.async_add_executor_job( - partial(self.get_wan_link_properties) - ) - async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" @@ -685,13 +678,6 @@ class AvmWrapper(FritzBoxTools): "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" ) - def get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return self._service_call_action( - "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" - ) - def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]: """Call SetEnable action from WLANConfiguration service.""" From 66e076b57f1b9e6977c8f12e0ba5a15333f5837a Mon Sep 17 00:00:00 2001 From: "M. Frister" Date: Sun, 6 Feb 2022 18:17:41 +0100 Subject: [PATCH 208/298] Bump soco to 0.26.2 (#65919) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index b482556f287..bd16701435c 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.26.0"], + "requirements": ["soco==0.26.2"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index d0b39f593c5..7f6e269746b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2228,7 +2228,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.0 +soco==0.26.2 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a87796f665c..901f753105d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1355,7 +1355,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.0 +soco==0.26.2 # homeassistant.components.solaredge solaredge==0.0.2 From ceae63d457eba1dfe90058556235a8fa5747f5f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Feb 2022 21:33:53 -0800 Subject: [PATCH 209/298] Fix UPNP access to SSDP info (#65728) --- homeassistant/components/upnp/device.py | 17 ++++++++++------- tests/components/upnp/test_init.py | 13 +++++++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index c2c92f06488..3231d34a342 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -12,7 +12,7 @@ from async_upnp_client.exceptions import UpnpError from async_upnp_client.profiles.igd import IgdDevice from homeassistant.components import ssdp -from homeassistant.components.ssdp import SsdpChange +from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -71,19 +71,22 @@ class Device: return device async def async_ssdp_callback( - self, headers: Mapping[str, Any], change: SsdpChange + self, service_info: SsdpServiceInfo, change: SsdpChange ) -> None: """SSDP callback, update if needed.""" - _LOGGER.debug("SSDP Callback, change: %s, headers: %s", change, headers) - if ssdp.ATTR_SSDP_LOCATION not in headers: + _LOGGER.debug( + "SSDP Callback, change: %s, headers: %s", change, service_info.ssdp_headers + ) + if service_info.ssdp_location is None: return - location = headers[ssdp.ATTR_SSDP_LOCATION] device = self._igd_device.device - if location == device.device_url: + if service_info.ssdp_location == device.device_url: return - new_upnp_device = await async_create_upnp_device(self.hass, location) + new_upnp_device = await async_create_upnp_device( + self.hass, service_info.ssdp_location + ) device.reinit(new_upnp_device) @property diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 39a63893e33..bd2096b59a0 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -45,8 +45,13 @@ async def test_reinitialize_device( # Reinit. new_location = "http://192.168.1.1:12345/desc.xml" - headers = { - ssdp.ATTR_SSDP_LOCATION: new_location, - } - await device.async_ssdp_callback(headers, ...) + await device.async_ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://192.168.1.1:12345/desc.xml", + upnp={}, + ), + ..., + ) assert device._igd_device.device.device_url == new_location From 49d60482781df8c43fd5d82e776004d855efac3e Mon Sep 17 00:00:00 2001 From: Tiernan Date: Tue, 8 Feb 2022 23:26:36 +1000 Subject: [PATCH 210/298] Fix TOD incorrectly determining the state between sunrise and sunset (#65884) * Fix TOD component incorrectly determining the state between sunrise and sunset (#30199) * TOD fix * Comment added * Review * Review * Review * Update time after day fix workaround for compatibility with current version. Only apply fix when using times and not when using sun events. Add unit test for behaviour. Co-authored-by: Nikolay Vasilchuk --- homeassistant/components/tod/binary_sensor.py | 15 +++++++++++++++ tests/components/tod/test_binary_sensor.py | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 5b4a6e12459..6f14d5735fb 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -161,6 +161,21 @@ class TodSensor(BinarySensorEntity): self._time_before = before_event_date + # We are calculating the _time_after value assuming that it will happen today + # But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00 + # If _time_before and _time_after are ahead of nowutc: + # _time_before is set to 12:00 next day + # _time_after is set to 23:00 today + # nowutc is set to 10:00 today + if ( + not is_sun_event(self._after) + and self._time_after > nowutc + and self._time_before > nowutc + timedelta(days=1) + ): + # remove one day from _time_before and _time_after + self._time_after -= timedelta(days=1) + self._time_before -= timedelta(days=1) + # Add offset to utc boundaries according to the configuration self._time_after += self._after_offset self._time_before += self._before_offset diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index ef8088d6aab..06f29436d6e 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -163,6 +163,25 @@ async def test_midnight_turnover_before_midnight_outside_period(hass): assert state.state == STATE_OFF +async def test_after_happens_tomorrow(hass): + """Test when both before and after are in the future, and after is later than before.""" + test_time = datetime(2019, 1, 10, 10, 00, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + {"platform": "tod", "name": "Night", "after": "23:00", "before": "12:00"} + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON + + async def test_midnight_turnover_after_midnight_outside_period(hass): """Test midnight turnover setting before midnight inside period .""" test_time = datetime(2019, 1, 10, 20, 0, 0, tzinfo=dt_util.UTC) From f08ebf5b7efe38d07ca27516261fb177df43dee8 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Feb 2022 01:56:24 -0600 Subject: [PATCH 211/298] Bump plexapi to 4.9.2 (#65972) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 5355dd252f8..2d45f1217a7 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.9.1", + "plexapi==4.9.2", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index 7f6e269746b..f9473705bf5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1255,7 +1255,7 @@ pillow==9.0.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.9.1 +plexapi==4.9.2 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 901f753105d..94da4832122 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ pilight==0.1.1 pillow==9.0.1 # homeassistant.components.plex -plexapi==4.9.1 +plexapi==4.9.2 # homeassistant.components.plex plexauth==0.0.6 From ac63a7e01ec184e12317dbafccb5f8be3c334640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 7 Feb 2022 16:11:04 +0100 Subject: [PATCH 212/298] Add diagnostics to Version integration (#65999) Co-authored-by: Martin Hjelmare --- .../components/version/diagnostics.py | 56 +++++++++++++++++++ tests/components/version/test_diagnostics.py | 36 ++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 homeassistant/components/version/diagnostics.py create mode 100644 tests/components/version/test_diagnostics.py diff --git a/homeassistant/components/version/diagnostics.py b/homeassistant/components/version/diagnostics.py new file mode 100644 index 00000000000..2ba31bc8870 --- /dev/null +++ b/homeassistant/components/version/diagnostics.py @@ -0,0 +1,56 @@ +"""Provides diagnostics for Version.""" +from __future__ import annotations + +from typing import Any + +from attr import asdict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + devices = [] + + registry_devices = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + for device in registry_devices: + entities = [] + + registry_entities = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + + for entity in registry_entities: + state_dict = None + if state := hass.states.get(entity.entity_id): + state_dict = dict(state.as_dict()) + state_dict.pop("context", None) + + entities.append({"entry": asdict(entity), "state": state_dict}) + + devices.append({"device": asdict(device), "entities": entities}) + + return { + "entry": config_entry.as_dict(), + "coordinator_data": { + "version": coordinator.version, + "version_data": coordinator.version_data, + }, + "devices": devices, + } diff --git a/tests/components/version/test_diagnostics.py b/tests/components/version/test_diagnostics.py new file mode 100644 index 00000000000..1c9c8df4c62 --- /dev/null +++ b/tests/components/version/test_diagnostics.py @@ -0,0 +1,36 @@ +"""Test version diagnostics.""" + + +from aioaseko import ClientSession + +from homeassistant.core import HomeAssistant + +from .common import MOCK_VERSION, setup_version_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, +) -> None: + """Test diagnostic information.""" + config_entry = await setup_version_integration(hass) + + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + assert diagnostics["entry"]["data"] == { + "name": "", + "channel": "stable", + "image": "default", + "board": "OVA", + "version_source": "Local installation", + "source": "local", + } + + assert diagnostics["coordinator_data"] == { + "version": MOCK_VERSION, + "version_data": None, + } + assert len(diagnostics["devices"]) == 1 From 7195372616ffd46a632e163b95b8473de8d5b87d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Feb 2022 00:46:40 +0100 Subject: [PATCH 213/298] Suppress unwanted error messages during recorder migration (#66004) --- .../components/recorder/migration.py | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index b8f15a811db..48dca4d42ed 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -101,15 +101,15 @@ def _create_index(instance, table_name, index_name): "be patient!", index_name, ) - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() index.create(connection) - except (InternalError, OperationalError, ProgrammingError) as err: - raise_if_exception_missing_str(err, ["already exists", "duplicate"]) - _LOGGER.warning( - "Index %s already exists on %s, continuing", index_name, table_name - ) + except (InternalError, OperationalError, ProgrammingError) as err: + raise_if_exception_missing_str(err, ["already exists", "duplicate"]) + _LOGGER.warning( + "Index %s already exists on %s, continuing", index_name, table_name + ) _LOGGER.debug("Finished creating %s", index_name) @@ -129,19 +129,19 @@ def _drop_index(instance, table_name, index_name): success = False # Engines like DB2/Oracle - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute(text(f"DROP INDEX {index_name}")) - except SQLAlchemyError: - pass - else: - success = True + except SQLAlchemyError: + pass + else: + success = True # Engines like SQLite, SQL Server if not success: - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute( text( @@ -150,15 +150,15 @@ def _drop_index(instance, table_name, index_name): ) ) ) - except SQLAlchemyError: - pass - else: - success = True + except SQLAlchemyError: + pass + else: + success = True if not success: # Engines like MySQL, MS Access - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute( text( @@ -167,10 +167,10 @@ def _drop_index(instance, table_name, index_name): ) ) ) - except SQLAlchemyError: - pass - else: - success = True + except SQLAlchemyError: + pass + else: + success = True if success: _LOGGER.debug( @@ -203,8 +203,8 @@ def _add_columns(instance, table_name, columns_def): columns_def = [f"ADD {col_def}" for col_def in columns_def] - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute( text( @@ -214,14 +214,14 @@ def _add_columns(instance, table_name, columns_def): ) ) return - except (InternalError, OperationalError, ProgrammingError): - # Some engines support adding all columns at once, - # this error is when they don't - _LOGGER.info("Unable to use quick column add. Adding 1 by 1") + except (InternalError, OperationalError, ProgrammingError): + # Some engines support adding all columns at once, + # this error is when they don't + _LOGGER.info("Unable to use quick column add. Adding 1 by 1") for column_def in columns_def: - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute( text( @@ -230,13 +230,13 @@ def _add_columns(instance, table_name, columns_def): ) ) ) - except (InternalError, OperationalError, ProgrammingError) as err: - raise_if_exception_missing_str(err, ["already exists", "duplicate"]) - _LOGGER.warning( - "Column %s already exists on %s, continuing", - column_def.split(" ")[1], - table_name, - ) + except (InternalError, OperationalError, ProgrammingError) as err: + raise_if_exception_missing_str(err, ["already exists", "duplicate"]) + _LOGGER.warning( + "Column %s already exists on %s, continuing", + column_def.split(" ")[1], + table_name, + ) def _modify_columns(instance, engine, table_name, columns_def): @@ -271,8 +271,8 @@ def _modify_columns(instance, engine, table_name, columns_def): else: columns_def = [f"MODIFY {col_def}" for col_def in columns_def] - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute( text( @@ -282,12 +282,12 @@ def _modify_columns(instance, engine, table_name, columns_def): ) ) return - except (InternalError, OperationalError): - _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") + except (InternalError, OperationalError): + _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") for column_def in columns_def: - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute( text( @@ -296,10 +296,10 @@ def _modify_columns(instance, engine, table_name, columns_def): ) ) ) - except (InternalError, OperationalError): - _LOGGER.exception( - "Could not modify column %s in table %s", column_def, table_name - ) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not modify column %s in table %s", column_def, table_name + ) def _update_states_table_with_foreign_key_options(instance, engine): @@ -330,17 +330,17 @@ def _update_states_table_with_foreign_key_options(instance, engine): ) for alter in alters: - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute(DropConstraint(alter["old_fk"])) for fkc in states_key_constraints: if fkc.column_keys == alter["columns"]: connection.execute(AddConstraint(fkc)) - except (InternalError, OperationalError): - _LOGGER.exception( - "Could not update foreign options in %s table", TABLE_STATES - ) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not update foreign options in %s table", TABLE_STATES + ) def _drop_foreign_key_constraints(instance, engine, table, columns): @@ -361,16 +361,16 @@ def _drop_foreign_key_constraints(instance, engine, table, columns): ) for drop in drops: - try: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=instance.get_session()) as session: + try: connection = session.connection() connection.execute(DropConstraint(drop)) - except (InternalError, OperationalError): - _LOGGER.exception( - "Could not drop foreign constraints in %s table on %s", - TABLE_STATES, - columns, - ) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not drop foreign constraints in %s table on %s", + TABLE_STATES, + columns, + ) def _apply_update(instance, new_version, old_version): # noqa: C901 From 0f06ebde06ca22354ee38edac897c15762388c38 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Feb 2022 20:24:30 +0100 Subject: [PATCH 214/298] Revert "Make idle chromecasts appear as idle instead of off" (#66005) --- homeassistant/components/cast/media_player.py | 3 +- tests/components/cast/test_media_player.py | 30 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 975fa3f5836..d418373e599 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -50,6 +50,7 @@ from homeassistant.const import ( CAST_APP_ID_HOMEASSISTANT_LOVELACE, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, + STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) @@ -636,7 +637,7 @@ class CastDevice(MediaPlayerEntity): return STATE_PLAYING return STATE_IDLE if self._chromecast is not None and self._chromecast.is_idle: - return STATE_IDLE + return STATE_OFF return None @property diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index bd22a558314..51fe4a086a6 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -595,7 +595,7 @@ async def test_entity_availability(hass: HomeAssistant): conn_status_cb(connection_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "idle" + assert state.state == "off" connection_status = MagicMock() connection_status.status = "DISCONNECTED" @@ -624,7 +624,7 @@ async def test_entity_cast_status(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # No media status, pause, play, stop not supported @@ -642,8 +642,8 @@ async def test_entity_cast_status(hass: HomeAssistant): cast_status_cb(cast_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - # Volume not hidden even if no app is active - assert state.attributes.get("volume_level") == 0.5 + # Volume hidden if no app is active + assert state.attributes.get("volume_level") is None assert not state.attributes.get("is_volume_muted") chromecast.app_id = "1234" @@ -747,7 +747,7 @@ async def test_supported_features( state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert state.attributes.get("supported_features") == supported_features_no_media media_status = MagicMock(images=None) @@ -882,7 +882,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # Play_media @@ -928,7 +928,7 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # Play_media - cast with app ID @@ -970,7 +970,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # play_media - media_type cast with invalid JSON @@ -1042,7 +1042,7 @@ async def test_entity_media_content_type(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) media_status = MagicMock(images=None) @@ -1213,7 +1213,7 @@ async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # App id updated, but no media status @@ -1258,7 +1258,7 @@ async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media): cast_status_cb(cast_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "idle" + assert state.state == "off" # No cast status chromecast.is_idle = False @@ -1286,7 +1286,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) chromecast.app_id = CAST_APP_ID_HOMEASSISTANT_LOVELACE @@ -1326,7 +1326,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant): media_status_cb(media_status) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == "idle" + assert state.state == "off" chromecast.is_idle = False media_status_cb(media_status) @@ -1355,7 +1355,7 @@ async def test_group_media_states(hass, mz_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) group_media_status = MagicMock(images=None) @@ -1406,7 +1406,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock): state = hass.states.get(entity_id) assert state is not None assert state.name == "Speaker" - assert state.state == "idle" + assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) group_media_status = MagicMock(images=None) From 9734216215881635085df505c39ddb75e130cc08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 7 Feb 2022 16:04:18 +0100 Subject: [PATCH 215/298] Use strings directly instead of Enums in version config (#66007) --- .../components/version/config_flow.py | 23 +++++++++---------- homeassistant/components/version/const.py | 22 +++++++++--------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/version/config_flow.py b/homeassistant/components/version/config_flow.py index 30f03663de1..f37fa1c3da2 100644 --- a/homeassistant/components/version/config_flow.py +++ b/homeassistant/components/version/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from pyhaversion.consts import HaVersionChannel, HaVersionSource import voluptuous as vol from homeassistant import config_entries @@ -75,8 +74,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._entry_data.update(user_input) if not self.show_advanced_options or user_input[CONF_SOURCE] in ( - HaVersionSource.LOCAL, - HaVersionSource.HAIO, + "local", + "haio", ): return self.async_create_entry( title=self._config_entry_name, @@ -92,8 +91,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the version_source step.""" if user_input is None: if self._entry_data[CONF_SOURCE] in ( - HaVersionSource.SUPERVISOR, - HaVersionSource.CONTAINER, + "supervisor", + "container", ): data_schema = vol.Schema( { @@ -102,7 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): vol.In(VALID_CHANNELS), } ) - if self._entry_data[CONF_SOURCE] == HaVersionSource.SUPERVISOR: + if self._entry_data[CONF_SOURCE] == "supervisor": data_schema = data_schema.extend( { vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In( @@ -151,7 +150,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @property def _config_entry_name(self) -> str: """Return the name of the config entry.""" - if self._entry_data[CONF_SOURCE] == HaVersionSource.LOCAL: + if self._entry_data[CONF_SOURCE] == "local": return DEFAULT_NAME_CURRENT name = self._entry_data[CONF_VERSION_SOURCE] @@ -166,21 +165,21 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any: """Convert a key from the imported configuration.""" data = DEFAULT_CONFIGURATION.copy() if config.get(CONF_BETA): - data[CONF_CHANNEL] = HaVersionChannel.BETA + data[CONF_CHANNEL] = "beta" if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE: if source == SOURCE_HASSIO: - data[CONF_SOURCE] = HaVersionSource.SUPERVISOR + data[CONF_SOURCE] = "supervisor" data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS elif source == SOURCE_DOKCER: - data[CONF_SOURCE] = HaVersionSource.CONTAINER + data[CONF_SOURCE] = "container" data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB else: data[CONF_SOURCE] = source data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source] if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE: - if data[CONF_SOURCE] == HaVersionSource.CONTAINER: + if data[CONF_SOURCE] == "container": data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}" else: data[CONF_IMAGE] = config[CONF_IMAGE] @@ -188,7 +187,7 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any: if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME: data[CONF_NAME] = config[CONF_NAME] else: - if data[CONF_SOURCE] == HaVersionSource.LOCAL: + if data[CONF_SOURCE] == "local": data[CONF_NAME] = DEFAULT_NAME_CURRENT else: data[CONF_NAME] = DEFAULT_NAME_LATEST diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 8575b17a703..8f1005961e8 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -41,12 +41,12 @@ VERSION_SOURCE_VERSIONS: Final = "Home Assistant Versions" DEFAULT_BETA: Final = False DEFAULT_BOARD: Final = "OVA" -DEFAULT_CHANNEL: Final[HaVersionChannel] = HaVersionChannel.STABLE +DEFAULT_CHANNEL: Final = "stable" DEFAULT_IMAGE: Final = "default" DEFAULT_NAME_CURRENT: Final = "Current Version" DEFAULT_NAME_LATEST: Final = "Latest Version" DEFAULT_NAME: Final = "" -DEFAULT_SOURCE: Final[HaVersionSource] = HaVersionSource.LOCAL +DEFAULT_SOURCE: Final = "local" DEFAULT_CONFIGURATION: Final[dict[str, Any]] = { CONF_NAME: DEFAULT_NAME, CONF_CHANNEL: DEFAULT_CHANNEL, @@ -81,22 +81,22 @@ BOARD_MAP: Final[dict[str, str]] = { VALID_BOARDS: Final[list[str]] = list(BOARD_MAP) -VERSION_SOURCE_MAP: Final[dict[str, HaVersionSource]] = { - VERSION_SOURCE_LOCAL: HaVersionSource.LOCAL, - VERSION_SOURCE_VERSIONS: HaVersionSource.SUPERVISOR, - VERSION_SOURCE_HAIO: HaVersionSource.HAIO, - VERSION_SOURCE_DOCKER_HUB: HaVersionSource.CONTAINER, - VERSION_SOURCE_PYPI: HaVersionSource.PYPI, +VERSION_SOURCE_MAP: Final[dict[str, str]] = { + VERSION_SOURCE_LOCAL: "local", + VERSION_SOURCE_VERSIONS: "supervisor", + VERSION_SOURCE_HAIO: "haio", + VERSION_SOURCE_DOCKER_HUB: "container", + VERSION_SOURCE_PYPI: "pypi", } -VERSION_SOURCE_MAP_INVERTED: Final[dict[HaVersionSource, str]] = { +VERSION_SOURCE_MAP_INVERTED: Final[dict[str, str]] = { value: key for key, value in VERSION_SOURCE_MAP.items() } VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [ - SOURCE_HASSIO, # Kept to not break existing configurations - SOURCE_DOKCER, # Kept to not break existing configurations + "hassio", # Kept to not break existing configurations + "docker", # Kept to not break existing configurations ] VALID_IMAGES: Final = [ From 02cb879717c7c3737e0783c7fa63002d29f56fa7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Feb 2022 18:11:52 +0100 Subject: [PATCH 216/298] Speed up deletion of duplicated statistics (#66014) --- .../components/recorder/statistics.py | 15 +- tests/components/recorder/test_statistics.py | 173 ++++++++++++++++++ 2 files changed, 181 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0bf10ca71c6..6c305242f5f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -290,7 +290,7 @@ def _find_duplicates( ) .filter(subquery.c.is_duplicate == 1) .order_by(table.metadata_id, table.start, table.id.desc()) - .limit(MAX_ROWS_TO_PURGE) + .limit(1000 * MAX_ROWS_TO_PURGE) ) duplicates = execute(query) original_as_dict = {} @@ -343,12 +343,13 @@ def _delete_duplicates_from_table( if not duplicate_ids: break all_non_identical_duplicates.extend(non_identical_duplicates) - deleted_rows = ( - session.query(table) - .filter(table.id.in_(duplicate_ids)) - .delete(synchronize_session=False) - ) - total_deleted_rows += deleted_rows + for i in range(0, len(duplicate_ids), MAX_ROWS_TO_PURGE): + deleted_rows = ( + session.query(table) + .filter(table.id.in_(duplicate_ids[i : i + MAX_ROWS_TO_PURGE])) + .delete(synchronize_session=False) + ) + total_deleted_rows += deleted_rows return (total_deleted_rows, all_non_identical_duplicates) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 25590c712d9..c96465a671f 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -855,6 +855,179 @@ def test_delete_duplicates(caplog, tmpdir): assert "Found duplicated" not in caplog.text +def test_delete_duplicates_many(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_23" + importlib.import_module(module) + old_models = sys.modules[module] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.create_engine", new=_create_engine_test + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for _ in range(3000): + session.add( + recorder.models.Statistics.from_stats( + 1, external_energy_statistics_1[-1] + ) + ) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + for stat in external_co2_statistics: + session.add(recorder.models.Statistics.from_stats(3, stat)) + + hass.stop() + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + + assert "Deleted 3002 duplicated statistics rows" in caplog.text + assert "Found non identical" not in caplog.text + assert "Found duplicated" not in caplog.text + + @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") def test_delete_duplicates_non_identical(caplog, tmpdir): """Test removal of duplicated statistics.""" From 715fe95abd00cc1cadeeda9f2f19a421ef50d6a4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Feb 2022 18:00:57 -0600 Subject: [PATCH 217/298] Clean up Sonos unsubscribe/resubscribe exception handling and logging (#66025) --- homeassistant/components/sonos/speaker.py | 44 +++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e40fe901b09..ca1e5a0a91c 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -399,13 +399,20 @@ class SonosSpeaker: return_exceptions=True, ) for result in results: - if isinstance(result, Exception): - _LOGGER.debug( - "Unsubscribe failed for %s: %s", - self.zone_name, - result, - exc_info=result, - ) + if isinstance(result, asyncio.exceptions.TimeoutError): + message = "Request timed out" + exc_info = None + elif isinstance(result, Exception): + message = result + exc_info = result if not str(result) else None + else: + continue + _LOGGER.debug( + "Unsubscribe failed for %s: %s", + self.zone_name, + message, + exc_info=exc_info, + ) self._subscriptions = [] @callback @@ -422,19 +429,18 @@ class SonosSpeaker: if not self.available: return - if getattr(exception, "status", None) == 412: - _LOGGER.warning( - "Subscriptions for %s failed, speaker may have lost power", - self.zone_name, - ) + if isinstance(exception, asyncio.exceptions.TimeoutError): + message = "Request timed out" + exc_info = None else: - exc_info = exception if _LOGGER.isEnabledFor(logging.DEBUG) else None - _LOGGER.error( - "Subscription renewals for %s failed: %s", - self.zone_name, - exception, - exc_info=exc_info, - ) + message = exception + exc_info = exception if not str(exception) else None + _LOGGER.warning( + "Subscription renewals for %s failed, marking unavailable: %s", + self.zone_name, + message, + exc_info=exc_info, + ) await self.async_offline() @callback From c8c1543b26da227f8811e934cb8fc2b1a12cd56c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Feb 2022 17:45:50 -0600 Subject: [PATCH 218/298] Fix decoding discovery with old Magic Home firmwares (#66038) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4e6cf97fa31..40661f27bab 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.21"], + "requirements": ["flux_led==0.28.22"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f9473705bf5..94de7f855dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.21 +flux_led==0.28.22 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94da4832122..c55531e1ba7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.21 +flux_led==0.28.22 # homeassistant.components.homekit fnvhash==0.1.0 From e53227be7992b7828e445011432a89802093d6d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Feb 2022 00:47:23 +0100 Subject: [PATCH 219/298] Fix race in MQTT sensor and binary_sensor expire_after (#66040) --- homeassistant/components/mqtt/binary_sensor.py | 5 ++++- homeassistant/components/mqtt/sensor.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index e26fe0b0259..aad73cd9f1a 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -133,6 +133,10 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self._expired = False self._state = last_state.state + if self._expiration_trigger: + # We might have set up a trigger already after subscribing from + # super().async_added_to_hass() + self._expiration_trigger() self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at ) @@ -189,7 +193,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): # Reset old trigger if self._expiration_trigger: self._expiration_trigger() - self._expiration_trigger = None # Set new trigger expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index fa949009a0d..6dddf496e02 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -180,6 +180,10 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): self._expired = False self._state = last_state.state + if self._expiration_trigger: + # We might have set up a trigger already after subscribing from + # super().async_added_to_hass() + self._expiration_trigger() self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at ) @@ -227,7 +231,6 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): # Reset old trigger if self._expiration_trigger: self._expiration_trigger() - self._expiration_trigger = None # Set new trigger expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) From c1cb0a0f8ef19b72f022346c9ed3dd60798bbf45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Feb 2022 17:45:40 -0600 Subject: [PATCH 220/298] Fix missing exception catch in august to prevent failed setup (#66045) --- homeassistant/components/august/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 5d51017bfd6..9b340096fde 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from err except asyncio.TimeoutError as err: raise ConfigEntryNotReady("Timed out connecting to august api") from err - except (ClientResponseError, CannotConnect) as err: + except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err: raise ConfigEntryNotReady from err From 23d21689520e1c88de89a553492557b7a1b38beb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 8 Feb 2022 00:53:48 -0500 Subject: [PATCH 221/298] Fix schema for zwave_js WS API (#66052) --- homeassistant/components/zwave_js/api.py | 3 +++ tests/components/zwave_js/test_api.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index ee0d4eb43a3..6208091dd8d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -133,6 +133,7 @@ APPLICATION_VERSION = "application_version" MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval" UUID = "uuid" SUPPORTED_PROTOCOLS = "supported_protocols" +ADDITIONAL_PROPERTIES = "additional_properties" FEATURE = "feature" UNPROVISION = "unprovision" @@ -170,6 +171,7 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), uuid=info.get(UUID), supported_protocols=protocols if protocols else None, + additional_properties=info.get(ADDITIONAL_PROPERTIES, {}), ) return info @@ -212,6 +214,7 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(Protocols)], ), + vol.Optional(ADDITIONAL_PROPERTIES): dict, } ), convert_qr_provisioning_information, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 40f60b9018a..f93ba4fbb93 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -29,6 +29,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( + ADDITIONAL_PROPERTIES, APPLICATION_VERSION, CLIENT_SIDE_AUTH, COMMAND_CLASS_ID, @@ -837,6 +838,7 @@ async def test_provision_smart_start_node(hass, integration, client, hass_ws_cli PRODUCT_TYPE: 1, PRODUCT_ID: 1, APPLICATION_VERSION: "test", + ADDITIONAL_PROPERTIES: {"name": "test"}, }, } ) @@ -861,6 +863,7 @@ async def test_provision_smart_start_node(hass, integration, client, hass_ws_cli max_inclusion_request_interval=None, uuid=None, supported_protocols=None, + additional_properties={"name": "test"}, ).to_dict(), } From 550f80ddd27e9234a8b99d4c04640ee6a86265a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Feb 2022 12:03:54 -0800 Subject: [PATCH 222/298] Bumped version to 2022.2.4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c2939e0b1f..dd09308b3b5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 8d9f8974eb7..6ade0983650 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.3 +version = 2022.2.4 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 6ec09320ddcb5abc677af33d88d45dc147291adc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Feb 2022 21:49:25 +0100 Subject: [PATCH 223/298] Fix cleanup of MQTT debug info (#66104) --- homeassistant/components/mqtt/__init__.py | 2 ++ homeassistant/components/mqtt/debug_info.py | 24 ++++++++++----------- homeassistant/components/mqtt/mixins.py | 2 +- tests/components/mqtt/test_init.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 39e8d0d55b3..f4613018b20 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -595,6 +595,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) + debug_info.initialize(hass) + return True diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 3e32d301b70..2b9172f0c9c 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -15,6 +15,11 @@ DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 +def initialize(hass: HomeAssistant): + """Initialize MQTT debug info.""" + hass.data[DATA_MQTT_DEBUG_INFO] = {"entities": {}, "triggers": {}} + + def log_messages( hass: HomeAssistant, entity_id: str ) -> Callable[[MessageCallbackType], MessageCallbackType]: @@ -45,9 +50,7 @@ def log_messages( def add_subscription(hass, message_callback, subscription): """Prepare debug data for subscription.""" if entity_id := getattr(message_callback, "__entity_id", None): - debug_info = hass.data.setdefault( - DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} - ) + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] entity_info = debug_info["entities"].setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}} ) @@ -76,9 +79,7 @@ def remove_subscription(hass, message_callback, subscription): def add_entity_discovery_data(hass, discovery_data, entity_id): """Add discovery data.""" - debug_info = hass.data.setdefault( - DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} - ) + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] entity_info = debug_info["entities"].setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}} ) @@ -93,14 +94,13 @@ def update_entity_discovery_data(hass, discovery_payload, entity_id): def remove_entity_data(hass, entity_id): """Remove discovery data.""" - hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) + if entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: + hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id): """Add discovery data.""" - debug_info = hass.data.setdefault( - DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} - ) + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] debug_info["triggers"][discovery_hash] = { "device_id": device_id, "discovery_data": discovery_data, @@ -126,9 +126,7 @@ async def info_for_device(hass, device_id): entries = hass.helpers.entity_registry.async_entries_for_device( entity_registry, device_id, include_disabled_entities=True ) - mqtt_debug_info = hass.data.setdefault( - DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} - ) + mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] for entry in entries: if entry.entity_id not in mqtt_debug_info["entities"]: continue diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 29676e4c9b9..47fc358dfdc 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -549,7 +549,6 @@ class MqttDiscoveryUpdate(Entity): def _cleanup_discovery_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" if self._discovery_data and not self._removed_from_hass: - debug_info.remove_entity_data(self.hass, self.entity_id) clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) self._removed_from_hass = True @@ -677,6 +676,7 @@ class MqttEntity( await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + debug_info.remove_entity_data(self.hass, self.entity_id) @staticmethod @abstractmethod diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9101b895218..1e11560cfc8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1764,7 +1764,7 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): } in discovery_data -async def test_debug_info_non_mqtt(hass, device_reg, entity_reg): +async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): """Test we get empty debug_info for a device with non MQTT entities.""" DOMAIN = "sensor" platform = getattr(hass.components, f"test.{DOMAIN}") From d5443b8dee81c51840a36aaee43775221f84628c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Feb 2022 23:00:53 +0100 Subject: [PATCH 224/298] Fix ENTITY_CATEGORIES_SCHEMA (#66108) Co-authored-by: Paulus Schoutsen --- homeassistant/components/knx/schema.py | 26 +++++++++---------- .../components/mobile_app/webhook.py | 4 +-- homeassistant/components/mqtt/mixins.py | 4 +-- homeassistant/helpers/entity.py | 10 +++++-- tests/helpers/test_entity.py | 25 ++++++++++++++++++ 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index af475e9c380..e946266f8f4 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -35,7 +35,7 @@ from homeassistant.const import ( Platform, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.entity import validate_entity_category from .const import ( CONF_INVERT, @@ -320,7 +320,7 @@ class BinarySensorSchema(KNXPlatformSchema): ), vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_RESET_AFTER): cv.positive_float, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), ) @@ -356,7 +356,7 @@ class ButtonSchema(KNXPlatformSchema): vol.Exclusive( CONF_TYPE, "length_or_type", msg=length_or_type_msg ): object, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), vol.Any( @@ -500,7 +500,7 @@ class ClimateSchema(KNXPlatformSchema): ): vol.In(HVAC_MODES), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), ) @@ -555,7 +555,7 @@ class CoverSchema(KNXPlatformSchema): vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), ) @@ -618,7 +618,7 @@ class FanSchema(KNXPlatformSchema): vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator, vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MAX_STEP): cv.byte, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ) @@ -722,7 +722,7 @@ class LightSchema(KNXPlatformSchema): vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), vol.Any( @@ -802,7 +802,7 @@ class NumberSchema(KNXPlatformSchema): vol.Optional(CONF_MAX): vol.Coerce(float), vol.Optional(CONF_MIN): vol.Coerce(float), vol.Optional(CONF_STEP): cv.positive_float, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), number_limit_sub_validator, @@ -824,7 +824,7 @@ class SceneSchema(KNXPlatformSchema): vol.Required(CONF_SCENE_NUMBER): vol.All( vol.Coerce(int), vol.Range(min=1, max=64) ), - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ) @@ -855,7 +855,7 @@ class SelectSchema(KNXPlatformSchema): ], vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), select_options_sub_validator, @@ -880,7 +880,7 @@ class SensorSchema(KNXPlatformSchema): vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Required(CONF_TYPE): sensor_type_validator, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ) @@ -901,7 +901,7 @@ class SwitchSchema(KNXPlatformSchema): vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ) @@ -948,7 +948,7 @@ class WeatherSchema(KNXPlatformSchema): vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, } ), ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index d093fd23406..d659d7625c1 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -44,7 +44,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.entity import validate_entity_category from homeassistant.util.decorator import Registry from .const import ( @@ -423,7 +423,7 @@ def _validate_state_class_sensor(value: dict): vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( None, bool, str, int, float ), - vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): validate_entity_category, vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES), }, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 47fc358dfdc..6b92ab91e31 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -30,11 +30,11 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import ( - ENTITY_CATEGORIES_SCHEMA, DeviceInfo, Entity, EntityCategory, async_generate_entity_id, + validate_entity_category, ) from homeassistant.helpers.typing import ConfigType @@ -191,7 +191,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, - vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a716e465450..b670c734b47 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -11,7 +11,7 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any, Final, Literal, TypedDict, final +from typing import Any, Literal, TypedDict, final import voluptuous as vol @@ -58,7 +58,13 @@ SOURCE_PLATFORM_CONFIG = "platform_config" FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 -ENTITY_CATEGORIES_SCHEMA: Final = vol.In(ENTITY_CATEGORIES) +def validate_entity_category(value: Any | None) -> EntityCategory: + """Validate entity category configuration.""" + value = vol.In(ENTITY_CATEGORIES)(value) + return EntityCategory(value) + + +ENTITY_CATEGORIES_SCHEMA = validate_entity_category @callback diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 1d28c50b9a1..6b7de074a24 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -6,6 +6,7 @@ import threading from unittest.mock import MagicMock, PropertyMock, patch import pytest +import voluptuous as vol from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -829,3 +830,27 @@ async def test_entity_category_property(hass): ) mock_entity2.entity_id = "hello.world" assert mock_entity2.entity_category == "config" + + +@pytest.mark.parametrize( + "value,expected", + ( + ("config", entity.EntityCategory.CONFIG), + ("diagnostic", entity.EntityCategory.DIAGNOSTIC), + ("system", entity.EntityCategory.SYSTEM), + ), +) +def test_entity_category_schema(value, expected): + """Test entity category schema.""" + schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) + result = schema(value) + assert result == expected + assert isinstance(result, entity.EntityCategory) + + +@pytest.mark.parametrize("value", (None, "non_existing")) +def test_entity_category_schema_error(value): + """Test entity category schema.""" + schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) + with pytest.raises(vol.Invalid): + schema(value) From f44ca5f9d501d973cfde24205da835d2345cae1b Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 8 Feb 2022 22:47:36 +0000 Subject: [PATCH 225/298] Fix generic camera typo in attr_frame_interval (#65390) --- homeassistant/components/generic/camera.py | 2 +- tests/components/generic/test_camera.py | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index b6084d148a3..b4aaad38618 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -96,7 +96,7 @@ class GenericCamera(Camera): if self._stream_source is not None: self._stream_source.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] - self._attr_frames_interval = 1 / device_info[CONF_FRAMERATE] + self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE] self._supported_features = SUPPORT_STREAM if self._stream_source else 0 self.content_type = device_info[CONF_CONTENT_TYPE] self.verify_ssl = device_info[CONF_VERIFY_SSL] diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 60e68a1e7b1..042cd2ee650 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -9,6 +9,7 @@ import pytest import respx from homeassistant import config as hass_config +from homeassistant.components.camera import async_get_mjpeg_stream from homeassistant.components.generic import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import SERVICE_RELOAD @@ -515,3 +516,29 @@ async def test_no_still_image_url(hass, hass_client): mock_stream.async_get_image.assert_called_once() assert resp.status == HTTPStatus.OK assert await resp.read() == b"stream_keyframe_image" + + +async def test_frame_interval_property(hass): + """Test that the frame interval is calculated and returned correctly.""" + + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + "framerate": 5, + }, + }, + ) + await hass.async_block_till_done() + + request = Mock() + with patch( + "homeassistant.components.camera.async_get_still_stream" + ) as mock_get_stream: + await async_get_mjpeg_stream(hass, request, "camera.config_test") + + assert mock_get_stream.call_args_list[0][0][3] == pytest.approx(0.2) From 339fc0a2afefa83613fec649669fce9e85f7cfd7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Feb 2022 09:23:32 +0100 Subject: [PATCH 226/298] Fix flaky homewizard test (#65490) --- tests/components/homewizard/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 6f5396f8702..c1a98c07108 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -663,10 +663,10 @@ async def test_sensors_unreachable(hass, mock_config_entry_data, mock_config_ent entry = mock_config_entry entry.data = mock_config_entry_data entry.add_to_hass(hass) - utcnow = dt_util.utcnow() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + utcnow = dt_util.utcnow() # Time after the integration is setup assert ( hass.states.get( @@ -717,7 +717,7 @@ async def test_api_disabled(hass, mock_config_entry_data, mock_config_entry): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - utcnow = dt_util.utcnow() + utcnow = dt_util.utcnow() # Time after the integration is setup assert ( hass.states.get( From bebdaacf4782bce057023c1f84f37f0d0bfb42bf Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 9 Feb 2022 09:44:04 +0100 Subject: [PATCH 227/298] Change detection of router devices for Fritz (#65965) --- homeassistant/components/fritz/common.py | 13 +++++++++++-- homeassistant/components/fritz/diagnostics.py | 1 + homeassistant/components/fritz/switch.py | 12 ++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 078bb4b4225..ae506989e00 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -155,7 +155,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self.hass = hass self.host = host self.mesh_role = MeshRoles.NONE - self.device_is_router: bool = True + self.device_conn_type: str | None = None + self.device_is_router: bool = False self.password = password self.port = port self.username = username @@ -213,7 +214,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self._current_firmware = info.get("NewSoftwareVersion") self._update_available, self._latest_firmware = self._update_device_info() - self.device_is_router = "WANIPConn1" in self.connection.services + if "Layer3Forwarding1" in self.connection.services: + if connection_type := self.connection.call_action( + "Layer3Forwarding1", "GetDefaultConnectionService" + ).get("NewDefaultConnectionService"): + # Return NewDefaultConnectionService sample: "1.WANPPPConnection.1" + self.device_conn_type = connection_type[2:][:-2] + self.device_is_router = self.connection.call_action( + self.device_conn_type, "GetInfo" + ).get("NewEnable") @callback async def _async_update_data(self) -> None: diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index f35eca6b914..fa4ff6a7db8 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -25,6 +25,7 @@ async def async_get_config_entry_diagnostics( "current_firmware": avm_wrapper.current_firmware, "latest_firmware": avm_wrapper.latest_firmware, "update_available": avm_wrapper.update_available, + "connection_type": avm_wrapper.device_conn_type, "is_router": avm_wrapper.device_is_router, "mesh_role": avm_wrapper.mesh_role, "last_update success": avm_wrapper.last_update_success, diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index fec760fbe7a..daddae5720c 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -81,16 +81,12 @@ def port_entities_list( _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PORTFORWARD) entities_list: list[FritzBoxPortSwitch] = [] - connection_type = avm_wrapper.get_default_connection() - if not connection_type: + if not avm_wrapper.device_conn_type: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_PORTFORWARD) return [] - # Return NewDefaultConnectionService sample: "1.WANPPPConnection.1" - con_type: str = connection_type["NewDefaultConnectionService"][2:][:-2] - # Query port forwardings and setup a switch for each forward for the current device - resp = avm_wrapper.get_num_port_mapping(con_type) + resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type) if not resp: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -107,7 +103,7 @@ def port_entities_list( for i in range(port_forwards_count): - portmap = avm_wrapper.get_port_mapping(con_type, i) + portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i) if not portmap: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) continue @@ -133,7 +129,7 @@ def port_entities_list( portmap, port_name, i, - con_type, + avm_wrapper.device_conn_type, ) ) From ae5a885387defbd59baeffd727fa3786bbc1f1b6 Mon Sep 17 00:00:00 2001 From: Richard Benson Date: Wed, 9 Feb 2022 04:25:07 -0500 Subject: [PATCH 228/298] Bump amcrest to 1.9.4 (#66124) Co-authored-by: Franck Nijhof --- homeassistant/components/amcrest/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 0d6c1380c20..1431b10a638 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.9.3"], + "requirements": ["amcrest==1.9.4"], "dependencies": ["ffmpeg"], "codeowners": ["@flacjacket"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 94de7f855dd..738dc269f5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,7 +308,7 @@ amberelectric==1.0.3 ambiclimate==0.2.1 # homeassistant.components.amcrest -amcrest==1.9.3 +amcrest==1.9.4 # homeassistant.components.androidtv androidtv[async]==0.0.63 From 200e07b8d6c8997633322a36f6397328c80e6d11 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 9 Feb 2022 10:02:21 +0100 Subject: [PATCH 229/298] Fix system is loaded flag during reboot/shutdown of Synology DSM (#66125) --- homeassistant/components/synology_dsm/common.py | 9 ++++++++- homeassistant/components/synology_dsm/service.py | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 54a0735186f..e27c7475251 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -33,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback -from .const import CONF_DEVICE_TOKEN +from .const import CONF_DEVICE_TOKEN, DOMAIN, SYSTEM_LOADED LOGGER = logging.getLogger(__name__) @@ -218,6 +218,11 @@ class SynoApi: ) self.surveillance_station = self.dsm.surveillance_station + def _set_system_loaded(self, state: bool = False) -> None: + """Set system loaded flag.""" + dsm_device = self._hass.data[DOMAIN].get(self.information.serial) + dsm_device[SYSTEM_LOADED] = state + async def _syno_api_executer(self, api_call: Callable) -> None: """Synology api call wrapper.""" try: @@ -231,10 +236,12 @@ class SynoApi: async def async_reboot(self) -> None: """Reboot NAS.""" await self._syno_api_executer(self.system.reboot) + self._set_system_loaded() async def async_shutdown(self) -> None: """Shutdown NAS.""" await self._syno_api_executer(self.system.shutdown) + self._set_system_loaded() async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" diff --git a/homeassistant/components/synology_dsm/service.py b/homeassistant/components/synology_dsm/service.py index f26a43b2ca0..a7a336e0c1b 100644 --- a/homeassistant/components/synology_dsm/service.py +++ b/homeassistant/components/synology_dsm/service.py @@ -15,7 +15,6 @@ from .const import ( SERVICE_SHUTDOWN, SERVICES, SYNO_API, - SYSTEM_LOADED, ) LOGGER = logging.getLogger(__name__) @@ -57,7 +56,6 @@ async def async_setup_services(hass: HomeAssistant) -> None: ) dsm_api: SynoApi = dsm_device[SYNO_API] try: - dsm_device[SYSTEM_LOADED] = False await getattr(dsm_api, f"async_{call.service}")() except SynologyDSMException as ex: LOGGER.error( From 4c548af6efd3a9230d88ff7e59725e28d984b326 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 9 Feb 2022 01:20:57 -0700 Subject: [PATCH 230/298] Bump simplisafe-python to 2022.02.1 (#66140) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 62b5b9aa7b7..a5a4b5f4821 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.02.0"], + "requirements": ["simplisafe-python==2022.02.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 738dc269f5b..a50bc68b199 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.02.0 +simplisafe-python==2022.02.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c55531e1ba7..a5bd1a6443c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.02.0 +simplisafe-python==2022.02.1 # homeassistant.components.slack slackclient==2.5.0 From 7dd7c1dadd3f8008565ca76a57d15d109e4973c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Feb 2022 09:43:03 +0100 Subject: [PATCH 231/298] Fix MQTT debug info (#66146) --- homeassistant/components/mqtt/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f4613018b20..5663f343296 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -577,6 +577,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_remove_device) websocket_api.async_register_command(hass, websocket_mqtt_info) + debug_info.initialize(hass) if conf is None: # If we have a config entry, setup is done by that config entry. @@ -595,8 +596,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - debug_info.initialize(hass) - return True From 2e6ee5165e80f96bdb202947b35e7712061aaa8a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Feb 2022 10:56:05 +0100 Subject: [PATCH 232/298] Bumped version to 2022.2.5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd09308b3b5..75cd20528bb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 6ade0983650..7d73060f322 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.4 +version = 2022.2.5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From caedef5f1a5fcebd454325b1d49caedd865832e3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Feb 2022 22:16:41 +0100 Subject: [PATCH 233/298] Reduce Spotify API usage (#66315) --- homeassistant/components/spotify/__init__.py | 33 +++++++++++++++++ homeassistant/components/spotify/const.py | 5 +++ .../components/spotify/media_player.py | 37 +++++++++++++++---- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 5c36a0c71c3..c3e9504c05c 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,6 +1,11 @@ """The spotify integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any import aiohttp +import requests from spotipy import Spotify, SpotifyException import voluptuous as vol @@ -20,13 +25,16 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( async_get_config_entry_implementation, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import config_flow from .const import ( DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_DEVICES, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN, + LOGGER, MEDIA_PLAYER_PREFIX, SPOTIFY_SCOPES, ) @@ -112,9 +120,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SpotifyException as err: raise ConfigEntryNotReady from err + async def _update_devices() -> list[dict[str, Any]]: + try: + devices: dict[str, Any] | None = await hass.async_add_executor_job( + spotify.devices + ) + except (requests.RequestException, SpotifyException) as err: + raise UpdateFailed from err + + if devices is None: + return [] + + return devices.get("devices", []) + + device_coordinator: DataUpdateCoordinator[ + list[dict[str, Any]] + ] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{entry.title} Devices", + update_interval=timedelta(minutes=5), + update_method=_update_devices, + ) + await device_coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_SPOTIFY_CLIENT: spotify, + DATA_SPOTIFY_DEVICES: device_coordinator, DATA_SPOTIFY_ME: current_user, DATA_SPOTIFY_SESSION: session, } diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index 7978ac8712f..0ed7cd2412e 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -1,8 +1,13 @@ """Define constants for the Spotify integration.""" +import logging + DOMAIN = "spotify" +LOGGER = logging.getLogger(__package__) + DATA_SPOTIFY_CLIENT = "spotify_client" +DATA_SPOTIFY_DEVICES = "spotify_devices" DATA_SPOTIFY_ME = "spotify_me" DATA_SPOTIFY_SESSION = "spotify_session" diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e279b150883..b3bb2efd1c0 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -52,7 +52,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.device_registry import DeviceEntryType @@ -62,6 +62,7 @@ from homeassistant.util.dt import utc_from_timestamp from .const import ( DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_DEVICES, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN, @@ -269,7 +270,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) self._currently_playing: dict | None = {} - self._devices: list[dict] | None = [] self._playlist: dict | None = None self._attr_name = self._name @@ -290,6 +290,11 @@ class SpotifyMediaPlayer(MediaPlayerEntity): """Return spotify API.""" return self._spotify_data[DATA_SPOTIFY_CLIENT] + @property + def _devices(self) -> list: + """Return spotify devices.""" + return self._spotify_data[DATA_SPOTIFY_DEVICES].data + @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -517,13 +522,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity): current = self._spotify.current_playback() self._currently_playing = current or {} - self._playlist = None context = self._currently_playing.get("context") - if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: - self._playlist = self._spotify.playlist(current["context"]["uri"]) - - devices = self._spotify.devices() or {} - self._devices = devices.get("devices", []) + if context is not None and ( + self._playlist is None or self._playlist["uri"] != context["uri"] + ): + self._playlist = None + if context["type"] == MEDIA_TYPE_PLAYLIST: + self._playlist = self._spotify.playlist(current["context"]["uri"]) async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" @@ -543,6 +548,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity): media_content_id, ) + @callback + def _handle_devices_update(self) -> None: + """Handle updated data from the coordinator.""" + if not self.enabled: + return + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self._spotify_data[DATA_SPOTIFY_DEVICES].async_add_listener( + self._handle_devices_update + ) + ) + async def async_browse_media_internal( hass, From 5976238126cee8b56e52d3b8fef345da30924507 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 9 Feb 2022 16:52:32 +0100 Subject: [PATCH 234/298] Fix hdmi-cec initialization (#66172) --- homeassistant/components/hdmi_cec/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 3d4851d8852..056eacb6a5b 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -191,6 +191,8 @@ def parse_mapping(mapping, parents=None): def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 """Set up the CEC capability.""" + hass.data[DOMAIN] = {} + # Parse configuration into a dict of device name to physical address # represented as a list of four elements. device_aliases = {} From 7cc9a4310d0403af70b177f4343e5897179de865 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Feb 2022 14:27:46 +0100 Subject: [PATCH 235/298] Fix controlling nested groups (#66176) --- homeassistant/components/group/cover.py | 2 + homeassistant/components/group/fan.py | 2 + homeassistant/components/group/light.py | 3 ++ tests/components/group/test_cover.py | 50 ++++++++++++++++++ tests/components/group/test_fan.py | 56 +++++++++++++++++++++ tests/components/group/test_light.py | 23 +++++++-- tests/components/group/test_media_player.py | 28 ++++++++--- 7 files changed, 155 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index a98f75fceb8..a4c550b8119 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -57,6 +57,8 @@ KEY_POSITION = "position" DEFAULT_NAME = "Cover Group" +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index cef30dc3c69..7920e0f5d20 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -52,6 +52,8 @@ SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE} DEFAULT_NAME = "Fan Group" +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 201156db600..ea74136b204 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -58,6 +58,9 @@ from .util import find_state_attributes, mean_tuple, reduce_attribute DEFAULT_NAME = "Light Group" +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index cf1fba992e7..d090141a9d2 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -1,6 +1,7 @@ """The tests for the group cover platform.""" from datetime import timedelta +import async_timeout import pytest from homeassistant.components.cover import ( @@ -735,3 +736,52 @@ async def test_is_opening_closing(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).state == STATE_OPENING assert hass.states.get(DEMO_COVER_POS).state == STATE_OPEN assert hass.states.get(COVER_GROUP).state == STATE_OPENING + + +async def test_nested_group(hass): + """Test nested cover group.""" + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + "entities": ["cover.bedroom_group"], + "name": "Nested Group", + }, + { + "platform": "group", + CONF_ENTITIES: [DEMO_COVER_POS, DEMO_COVER_TILT], + "name": "Bedroom Group", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.bedroom_group") + assert state is not None + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ENTITY_ID) == [DEMO_COVER_POS, DEMO_COVER_TILT] + + state = hass.states.get("cover.nested_group") + assert state is not None + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ENTITY_ID) == ["cover.bedroom_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.nested_group"}, + blocking=True, + ) + assert hass.states.get(DEMO_COVER_POS).state == STATE_CLOSING + assert hass.states.get(DEMO_COVER_TILT).state == STATE_CLOSING + assert hass.states.get("cover.bedroom_group").state == STATE_CLOSING + assert hass.states.get("cover.nested_group").state == STATE_CLOSING diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index abb1dcf245a..19b4fe4670a 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -1,6 +1,7 @@ """The tests for the group fan platform.""" from unittest.mock import patch +import async_timeout import pytest from homeassistant import config as hass_config @@ -497,3 +498,58 @@ async def test_service_calls(hass, setup_comp): assert percentage_full_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE fan_group_state = hass.states.get(FAN_GROUP) assert fan_group_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE + + +async def test_nested_group(hass): + """Test nested fan group.""" + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + "entities": ["fan.bedroom_group"], + "name": "Nested Group", + }, + { + "platform": "group", + CONF_ENTITIES: [ + LIVING_ROOM_FAN_ENTITY_ID, + PERCENTAGE_FULL_FAN_ENTITY_ID, + ], + "name": "Bedroom Group", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("fan.bedroom_group") + assert state is not None + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ENTITY_ID) == [ + LIVING_ROOM_FAN_ENTITY_ID, + PERCENTAGE_FULL_FAN_ENTITY_ID, + ] + + state = hass.states.get("fan.nested_group") + assert state is not None + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ENTITY_ID) == ["fan.bedroom_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.nested_group"}, + blocking=True, + ) + assert hass.states.get(LIVING_ROOM_FAN_ENTITY_ID).state == STATE_ON + assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_ON + assert hass.states.get("fan.bedroom_group").state == STATE_ON + assert hass.states.get("fan.nested_group").state == STATE_ON diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 843f15c7113..d356b20b40f 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -2,6 +2,7 @@ import unittest.mock from unittest.mock import MagicMock, patch +import async_timeout import pytest from homeassistant import config as hass_config @@ -1470,12 +1471,12 @@ async def test_reload_with_base_integration_platform_not_setup(hass): async def test_nested_group(hass): """Test nested light group.""" - hass.states.async_set("light.kitchen", "on") await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ + {"platform": "demo"}, { "platform": DOMAIN, "entities": ["light.bedroom_group"], @@ -1483,7 +1484,7 @@ async def test_nested_group(hass): }, { "platform": DOMAIN, - "entities": ["light.kitchen", "light.bedroom"], + "entities": ["light.bed_light", "light.kitchen_lights"], "name": "Bedroom Group", }, ] @@ -1496,9 +1497,25 @@ async def test_nested_group(hass): state = hass.states.get("light.bedroom_group") assert state is not None assert state.state == STATE_ON - assert state.attributes.get(ATTR_ENTITY_ID) == ["light.kitchen", "light.bedroom"] + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "light.bed_light", + "light.kitchen_lights", + ] state = hass.states.get("light.nested_group") assert state is not None assert state.state == STATE_ON assert state.attributes.get(ATTR_ENTITY_ID) == ["light.bedroom_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "light.nested_group"}, + blocking=True, + ) + assert hass.states.get("light.bed_light").state == STATE_OFF + assert hass.states.get("light.kitchen_lights").state == STATE_OFF + assert hass.states.get("light.bedroom_group").state == STATE_OFF + assert hass.states.get("light.nested_group").state == STATE_OFF diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 27962297952..f741e2d1a84 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -1,6 +1,7 @@ """The tests for the Media group platform.""" from unittest.mock import patch +import async_timeout import pytest from homeassistant.components.group import DOMAIN @@ -486,12 +487,12 @@ async def test_service_calls(hass, mock_media_seek): async def test_nested_group(hass): """Test nested media group.""" - hass.states.async_set("media_player.player_1", "on") await async_setup_component( hass, MEDIA_DOMAIN, { MEDIA_DOMAIN: [ + {"platform": "demo"}, { "platform": DOMAIN, "entities": ["media_player.group_1"], @@ -499,7 +500,7 @@ async def test_nested_group(hass): }, { "platform": DOMAIN, - "entities": ["media_player.player_1", "media_player.player_2"], + "entities": ["media_player.bedroom", "media_player.kitchen"], "name": "Group 1", }, ] @@ -511,13 +512,28 @@ async def test_nested_group(hass): state = hass.states.get("media_player.group_1") assert state is not None - assert state.state == STATE_ON + assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_ENTITY_ID) == [ - "media_player.player_1", - "media_player.player_2", + "media_player.bedroom", + "media_player.kitchen", ] state = hass.states.get("media_player.nested_group") assert state is not None - assert state.state == STATE_ON + assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_ENTITY_ID) == ["media_player.group_1"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "media_player.group_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_OFF + assert hass.states.get("media_player.kitchen").state == STATE_OFF + assert hass.states.get("media_player.group_1").state == STATE_OFF + assert hass.states.get("media_player.nested_group").state == STATE_OFF From 0199e8cc43afcb197d5555638c492d0b0b48e0b8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 9 Feb 2022 17:54:27 +0100 Subject: [PATCH 236/298] Bump aioesphomeapi from 10.8.1 to 10.8.2 (#66189) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 042bf930d0e..25e3abe9700 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.8.1"], + "requirements": ["aioesphomeapi==10.8.2"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index a50bc68b199..196dbe9c49c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -166,7 +166,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.8.1 +aioesphomeapi==10.8.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5bd1a6443c..21c1ffbbd9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.8.1 +aioesphomeapi==10.8.2 # homeassistant.components.flo aioflo==2021.11.0 From 854308fec21b16fdbdc058b64f9c759729754e89 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 10 Feb 2022 14:25:07 -0600 Subject: [PATCH 237/298] Handle more Sonos favorites in media browser (#66205) --- homeassistant/components/sonos/const.py | 3 +++ .../components/sonos/media_browser.py | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index abb0696360b..6aec401b412 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -44,6 +44,7 @@ SONOS_ALBUM_ARTIST = "album_artists" SONOS_TRACKS = "tracks" SONOS_COMPOSER = "composers" SONOS_RADIO = "radio" +SONOS_OTHER_ITEM = "other items" SONOS_STATE_PLAYING = "PLAYING" SONOS_STATE_TRANSITIONING = "TRANSITIONING" @@ -76,6 +77,7 @@ SONOS_TO_MEDIA_CLASSES = { "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, + "object.item": MEDIA_CLASS_TRACK, "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, "object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE, } @@ -121,6 +123,7 @@ SONOS_TYPES_MAPPING = { "object.container.person.musicArtist": SONOS_ALBUM_ARTIST, "object.container.playlistContainer.sameArtist": SONOS_ARTIST, "object.container.playlistContainer": SONOS_PLAYLISTS, + "object.item": SONOS_OTHER_ITEM, "object.item.audioItem.musicTrack": SONOS_TRACKS, "object.item.audioItem.audioBroadcast": SONOS_RADIO, } diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 2e3bf9d1fcb..2d971469928 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -162,8 +162,17 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): payload["idstring"].split("/")[2:] ) + try: + search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]] + except KeyError: + _LOGGER.debug( + "Unknown media type received when building item response: %s", + payload["search_type"], + ) + return + media = media_library.browse_by_idstring( - MEDIA_TYPES_TO_SONOS[payload["search_type"]], + search_type, payload["idstring"], full_album_art_uri=True, max_items=0, @@ -371,11 +380,16 @@ def favorites_payload(favorites): group_types = {fav.reference.item_class for fav in favorites} for group_type in sorted(group_types): - media_content_type = SONOS_TYPES_MAPPING[group_type] + try: + media_content_type = SONOS_TYPES_MAPPING[group_type] + media_class = SONOS_TO_MEDIA_CLASSES[group_type] + except KeyError: + _LOGGER.debug("Unknown media type or class received %s", group_type) + continue children.append( BrowseMedia( title=media_content_type.title(), - media_class=SONOS_TO_MEDIA_CLASSES[group_type], + media_class=media_class, media_content_id=group_type, media_content_type="favorites_folder", can_play=False, From a2e7897b1ebcae560db3aad066a98c3ecde26d36 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Wed, 9 Feb 2022 21:08:46 +0100 Subject: [PATCH 238/298] Add missing nina warnings (#66211) --- homeassistant/components/nina/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 12df703a480..18af5021544 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -26,7 +26,7 @@ CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"] CONST_LIST_I_TO_L: list[str] = ["I", "J", "K", "L"] CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"] CONST_LIST_R_TO_U: list[str] = ["R", "S", "T", "U", "Ü"] -CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y"] +CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y", "Z"] CONST_REGION_A_TO_D: Final = "_a_to_d" CONST_REGION_E_TO_H: Final = "_e_to_h" From eb781060e8973e208d7a9d549fa8339dd0a6bddf Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 10 Feb 2022 08:43:48 +0100 Subject: [PATCH 239/298] bump py-synologydsm-api to 1.0.6 (#66226) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index c436557cea9..124679c3516 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.5"], + "requirements": ["py-synologydsm-api==1.0.6"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 196dbe9c49c..351e4271aa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1331,7 +1331,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.5 +py-synologydsm-api==1.0.6 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21c1ffbbd9b..ab07d28ff57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -829,7 +829,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.5 +py-synologydsm-api==1.0.6 # homeassistant.components.seventeentrack py17track==2021.12.2 From 92bc780dd759e996ffa0eb6bf133e13da2bd05d2 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 10 Feb 2022 08:37:35 +0100 Subject: [PATCH 240/298] Bump aioaseko to 0.0.2 to fix issue (#66240) --- homeassistant/components/aseko_pool_live/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index f6323b49354..d1fc553a9ff 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -3,7 +3,7 @@ "name": "Aseko Pool Live", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", - "requirements": ["aioaseko==0.0.1"], + "requirements": ["aioaseko==0.0.2"], "codeowners": [ "@milanmeu" ], diff --git a/requirements_all.txt b/requirements_all.txt index 351e4271aa7..1b838e8b372 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ aio_georss_gdacs==0.5 aioambient==2021.11.0 # homeassistant.components.aseko_pool_live -aioaseko==0.0.1 +aioaseko==0.0.2 # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab07d28ff57..06475e17373 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_georss_gdacs==0.5 aioambient==2021.11.0 # homeassistant.components.aseko_pool_live -aioaseko==0.0.1 +aioaseko==0.0.2 # homeassistant.components.asuswrt aioasuswrt==1.4.0 From dfcad3a13d37a8b3f0d6508dcf127b9338fc6d94 Mon Sep 17 00:00:00 2001 From: ufodone <35497351+ufodone@users.noreply.github.com> Date: Fri, 11 Feb 2022 02:38:50 -0800 Subject: [PATCH 241/298] Disable zone bypass switch feature (#66243) * Add configuration option to disable the creation of zone bypass switches * Removed temporary workaround and bumped pyenvisalink version to pick up the correct fix. * Remove zone bypass configuration option and disable zone bypass switches per code review instructions. --- CODEOWNERS | 1 + .../components/envisalink/__init__.py | 18 ++++-------------- .../components/envisalink/manifest.json | 4 ++-- requirements_all.txt | 2 +- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e5b6ed9798a..88669e8b5c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -264,6 +264,7 @@ tests/components/enphase_envoy/* @gtdiehl homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @gwww @michaeldavie tests/components/environment_canada/* @gwww @michaeldavie +homeassistant/components/envisalink/* @ufodone homeassistant/components/ephember/* @ttroy50 homeassistant/components/epson/* @pszafer tests/components/epson/* @pszafer diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 183627fdfa6..aa276af492c 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_alive, hass.loop, connection_timeout, + False, ) hass.data[DATA_EVL] = controller @@ -181,12 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("The envisalink sent a partition update event") async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data) - @callback - def async_zone_bypass_update(data): - """Handle zone bypass status updates.""" - _LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones") - async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data) - @callback def stop_envisalink(event): """Shutdown envisalink connection and thread on exit.""" @@ -206,7 +201,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: controller.callback_login_failure = async_login_fail_callback controller.callback_login_timeout = async_connection_fail_callback controller.callback_login_success = async_connection_success_callback - controller.callback_zone_bypass_update = async_zone_bypass_update _LOGGER.info("Start envisalink") controller.start() @@ -240,13 +234,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config ) ) - # Only DSC panels support getting zone bypass status - if panel_type == PANEL_TYPE_DSC: - hass.async_create_task( - async_load_platform( - hass, "switch", "envisalink", {CONF_ZONES: zones}, config - ) - ) + + # Zone bypass switches are not currently created due to an issue with some panels. + # These switches will be re-added in the future after some further refactoring of the integration. hass.services.async_register( DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 25290a5d431..52ac06ff8c3 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.3"], - "codeowners": [], + "requirements": ["pyenvisalink==4.4"], + "codeowners": ["@ufodone"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 1b838e8b372..5b9e5f1dcd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1500,7 +1500,7 @@ pyeight==0.2.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.3 +pyenvisalink==4.4 # homeassistant.components.ephember pyephember==0.3.1 From 65c83633235be83d9227aae11fe184c49fd77562 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 11 Feb 2022 04:25:31 +0800 Subject: [PATCH 242/298] Catch ConnectionResetError when writing MJPEG in camera (#66245) --- homeassistant/components/camera/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 110cf11cde9..466dce1c84d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -222,7 +222,12 @@ async def async_get_mjpeg_stream( """Fetch an mjpeg stream from a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) - return await camera.handle_async_mjpeg_stream(request) + try: + stream = await camera.handle_async_mjpeg_stream(request) + except ConnectionResetError: + stream = None + _LOGGER.debug("Error while writing MJPEG stream to transport") + return stream async def async_get_still_stream( @@ -784,7 +789,11 @@ class CameraMjpegStream(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Serve camera stream, possibly with interval.""" if (interval_str := request.query.get("interval")) is None: - stream = await camera.handle_async_mjpeg_stream(request) + try: + stream = await camera.handle_async_mjpeg_stream(request) + except ConnectionResetError: + stream = None + _LOGGER.debug("Error while writing MJPEG stream to transport") if stream is None: raise web.HTTPBadGateway() return stream From 2594500452ba59e8056bcdee8f2f884d0cd68172 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 10 Feb 2022 21:59:42 +0100 Subject: [PATCH 243/298] Correct philips_js usage of the overloaded coordinator (#66287) --- homeassistant/components/philips_js/media_player.py | 7 +++---- homeassistant/components/philips_js/remote.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 1a3c6b52a0d..be24be632fc 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -82,7 +82,7 @@ async def async_setup_entry( class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" - _coordinator: PhilipsTVDataUpdateCoordinator + coordinator: PhilipsTVDataUpdateCoordinator _attr_device_class = MediaPlayerDeviceClass.TV def __init__( @@ -91,7 +91,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api - self._coordinator = coordinator self._sources = {} self._channels = {} self._supports = SUPPORT_PHILIPS_JS @@ -125,7 +124,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def supported_features(self): """Flag media player features that are supported.""" supports = self._supports - if self._coordinator.turn_on or ( + if self.coordinator.turn_on or ( self._tv.on and self._tv.powerstate is not None ): supports |= SUPPORT_TURN_ON @@ -170,7 +169,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): await self._tv.setPowerState("On") self._state = STATE_ON else: - await self._coordinator.turn_on.async_run(self.hass, self._context) + await self.coordinator.turn_on.async_run(self.hass, self._context) await self._async_update_soon() async def async_turn_off(self): diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 6bf60f7f5b0..09fe16215b6 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -30,7 +30,7 @@ async def async_setup_entry( class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): """Device that sends commands.""" - _coordinator: PhilipsTVDataUpdateCoordinator + coordinator: PhilipsTVDataUpdateCoordinator def __init__( self, @@ -63,7 +63,7 @@ class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") else: - await self._coordinator.turn_on.async_run(self.hass, self._context) + await self.coordinator.turn_on.async_run(self.hass, self._context) self.async_write_ha_state() async def async_turn_off(self, **kwargs): From 76872e37899982c2fd0913c8469235edf0e2f953 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Feb 2022 03:15:50 -0600 Subject: [PATCH 244/298] Fix august token refresh when data contains characters outside of latin1 (#66303) * WIP * bump version * bump --- homeassistant/components/august/__init__.py | 1 + homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 9b340096fde..8a7c0f93592 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -75,6 +75,7 @@ async def async_setup_august( hass.config_entries.async_update_entry(config_entry, data=config_data) await august_gateway.async_authenticate() + await august_gateway.async_refresh_access_token_if_needed() hass.data.setdefault(DOMAIN, {}) data = hass.data[DOMAIN][config_entry.entry_id] = { diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ea6769fabcf..0dfcb6094b1 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.20"], + "requirements": ["yalexs==1.1.22"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5b9e5f1dcd3..236abecdfe0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2513,7 +2513,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.7 # homeassistant.components.august -yalexs==1.1.20 +yalexs==1.1.22 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06475e17373..4271d855f5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1541,7 +1541,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.7 # homeassistant.components.august -yalexs==1.1.20 +yalexs==1.1.22 # homeassistant.components.yeelight yeelight==0.7.8 From 669c99474b13f6669697f7f3da8a805dd6154092 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Feb 2022 08:40:06 -0800 Subject: [PATCH 245/298] Bump python-nest to 4.2.0 for python 3.10 fixes (#66090) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 478e608700c..e1ae022a4ad 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==1.6.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.6.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 236abecdfe0..44220a07bf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1954,7 +1954,7 @@ python-mpd2==3.0.4 python-mystrom==1.1.2 # homeassistant.components.nest -python-nest==4.1.0 +python-nest==4.2.0 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4271d855f5d..6717a97609b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,7 +1206,7 @@ python-kasa==0.4.1 python-miio==0.5.9.2 # homeassistant.components.nest -python-nest==4.1.0 +python-nest==4.2.0 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 From 27752f7ad36da2630a5e5294832a2437314fb986 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 9 Feb 2022 00:12:49 -0800 Subject: [PATCH 246/298] Bump google-nest-sdm to 1.7.0 (#66145) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index e1ae022a4ad..45d976c03c6 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.6.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 44220a07bf4..09c6e4e979d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==1.6.0 +google-nest-sdm==1.7.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6717a97609b..673196688f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.9.0 # homeassistant.components.nest -google-nest-sdm==1.6.0 +google-nest-sdm==1.7.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 60b460001959424c407de569f3e3730028bdb8ea Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 11 Feb 2022 01:13:00 -0800 Subject: [PATCH 247/298] Bump google-nest-sdm to 1.7.1 (minor patch) (#66304) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 45d976c03c6..832c186db35 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 09c6e4e979d..41286e377ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==1.7.0 +google-nest-sdm==1.7.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 673196688f7..3bd1a2bbf86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.9.0 # homeassistant.components.nest -google-nest-sdm==1.7.0 +google-nest-sdm==1.7.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 27c5460febf03c3415531f1015136b58e8feae86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 11 Feb 2022 14:57:45 +0100 Subject: [PATCH 248/298] Add guard for invalid EntityCategory value (#66316) --- homeassistant/helpers/entity.py | 5 ++++- tests/helpers/test_entity_registry.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index b670c734b47..e9038d1f658 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -223,7 +223,10 @@ def convert_to_entity_category( "EntityCategory instead" % (type(value).__name__, value), error_if_core=False, ) - return EntityCategory(value) + try: + return EntityCategory(value) + except ValueError: + return None return value diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index f299177a08e..1eaac0e72bf 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1124,3 +1124,15 @@ async def test_deprecated_disabled_by_str(hass, registry, caplog): assert entry.disabled_by is er.RegistryEntryDisabler.USER assert " str for entity registry disabled_by. This is deprecated " in caplog.text + + +async def test_invalid_entity_category_str(hass, registry, caplog): + """Test use of invalid entity category.""" + entry = er.RegistryEntry( + "light", + "hue", + "5678", + entity_category="invalid", + ) + + assert entry.entity_category is None From aef2588f9c85e50d86caa3c2a01196f7ed60db94 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 7 Feb 2022 10:57:42 +0100 Subject: [PATCH 249/298] bump motionblinds to 0.5.11 (#65988) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 8636bd6ed94..90ae8cacbe9 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.10"], + "requirements": ["motionblinds==0.5.11"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 41286e377ec..b3634333d1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1049,7 +1049,7 @@ minio==5.0.10 mitemp_bt==0.0.5 # homeassistant.components.motion_blinds -motionblinds==0.5.10 +motionblinds==0.5.11 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bd1a2bbf86..b41544b424d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ millheater==0.9.0 minio==5.0.10 # homeassistant.components.motion_blinds -motionblinds==0.5.10 +motionblinds==0.5.11 # homeassistant.components.motioneye motioneye-client==0.3.12 From 6857562e9e6999b1d44ed253549330af5218894a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 11 Feb 2022 15:49:54 +0100 Subject: [PATCH 250/298] bump motionblinds to 0.5.12 (#66323) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 90ae8cacbe9..fc664910c84 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.11"], + "requirements": ["motionblinds==0.5.12"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index b3634333d1e..dc022f016f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1049,7 +1049,7 @@ minio==5.0.10 mitemp_bt==0.0.5 # homeassistant.components.motion_blinds -motionblinds==0.5.11 +motionblinds==0.5.12 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b41544b424d..82235fde402 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ millheater==0.9.0 minio==5.0.10 # homeassistant.components.motion_blinds -motionblinds==0.5.11 +motionblinds==0.5.12 # homeassistant.components.motioneye motioneye-client==0.3.12 From 6084b323dfaf2251fff5dccfa89365757c902e16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Feb 2022 11:07:32 -0600 Subject: [PATCH 251/298] Reduce number of parallel api calls to august (#66328) --- homeassistant/components/august/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 8a7c0f93592..031f513843f 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -107,11 +107,10 @@ class AugustData(AugustSubscriberMixin): async def async_setup(self): """Async setup of august device data and activities.""" token = self._august_gateway.access_token - user_data, locks, doorbells = await asyncio.gather( - self._api.async_get_user(token), - self._api.async_get_operable_locks(token), - self._api.async_get_doorbells(token), - ) + # This used to be a gather but it was less reliable with august's recent api changes. + user_data = await self._api.async_get_user(token) + locks = await self._api.async_get_operable_locks(token) + doorbells = await self._api.async_get_doorbells(token) if not doorbells: doorbells = [] if not locks: From fcee1ff865b48e3c19f645692c9cf72f8129604b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 11 Feb 2022 18:08:19 +0100 Subject: [PATCH 252/298] Fix raspihats initialization (#66330) Co-authored-by: epenet --- homeassistant/components/raspihats/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 3d28086db43..8f4a8b0aca4 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -40,7 +40,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" ) - hass.data[DOMAIN][I2C_HATS_MANAGER] = I2CHatsManager() + hass.data[DOMAIN] = {I2C_HATS_MANAGER: I2CHatsManager()} def start_i2c_hats_keep_alive(event): """Start I2C-HATs keep alive.""" From 087f443368116888c0b104c0f989ec698ff01152 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 11 Feb 2022 13:17:19 -0800 Subject: [PATCH 253/298] Fix nest streams that get stuck broken (#66334) --- homeassistant/components/stream/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index b6bfd2122ba..79506c0bda2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -338,7 +338,6 @@ class Stream: ) except StreamWorkerError as err: self._logger.error("Error from stream worker: %s", str(err)) - self._available = False stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): From f3a3ff28f2f219df56214a1f6a76ac4a45c9fe98 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Feb 2022 19:11:06 +0100 Subject: [PATCH 254/298] Fix PVOutput when no data is available (#66338) --- .../components/pvoutput/config_flow.py | 2 +- .../components/pvoutput/coordinator.py | 6 ++-- .../components/pvoutput/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/pvoutput/test_config_flow.py | 28 +++++++++---------- tests/components/pvoutput/test_init.py | 10 +++++-- 7 files changed, 30 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index a1933ff9315..53eabe225f6 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -23,7 +23,7 @@ async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) - api_key=api_key, system_id=system_id, ) - await pvoutput.status() + await pvoutput.system() class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/pvoutput/coordinator.py b/homeassistant/components/pvoutput/coordinator.py index cadef8c8a0d..7b307f20274 100644 --- a/homeassistant/components/pvoutput/coordinator.py +++ b/homeassistant/components/pvoutput/coordinator.py @@ -1,14 +1,14 @@ """DataUpdateCoordinator for the PVOutput integration.""" from __future__ import annotations -from pvo import PVOutput, PVOutputAuthenticationError, Status +from pvo import PVOutput, PVOutputAuthenticationError, PVOutputNoDataError, Status from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER, SCAN_INTERVAL @@ -33,5 +33,7 @@ class PVOutputDataUpdateCoordinator(DataUpdateCoordinator[Status]): """Fetch system status from PVOutput.""" try: return await self.pvoutput.status() + except PVOutputNoDataError as err: + raise UpdateFailed("PVOutput has no data available") from err except PVOutputAuthenticationError as err: raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 042c6b9aa99..021fffe0e01 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/pvoutput", "config_flow": true, "codeowners": ["@fabaff", "@frenck"], - "requirements": ["pvo==0.2.1"], + "requirements": ["pvo==0.2.2"], "iot_class": "cloud_polling", "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index dc022f016f6..b524400e811 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1310,7 +1310,7 @@ pushbullet.py==0.11.0 pushover_complete==1.1.1 # homeassistant.components.pvoutput -pvo==0.2.1 +pvo==0.2.2 # homeassistant.components.rpi_gpio_pwm pwmled==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82235fde402..358a1d21748 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -814,7 +814,7 @@ pure-python-adb[async]==0.3.0.dev0 pushbullet.py==0.11.0 # homeassistant.components.pvoutput -pvo==0.2.1 +pvo==0.2.2 # homeassistant.components.canary py-canary==0.5.1 diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 0c060a75a9d..8cd776beea3 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_user_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_full_flow_with_authentication_error( @@ -68,7 +68,7 @@ async def test_full_flow_with_authentication_error( assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - mock_pvoutput_config_flow.status.side_effect = PVOutputAuthenticationError + mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -83,9 +83,9 @@ async def test_full_flow_with_authentication_error( assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 - mock_pvoutput_config_flow.status.side_effect = None + mock_pvoutput_config_flow.system.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={ @@ -102,14 +102,14 @@ async def test_full_flow_with_authentication_error( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 2 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 2 async def test_connection_error( hass: HomeAssistant, mock_pvoutput_config_flow: MagicMock ) -> None: """Test API connection error.""" - mock_pvoutput_config_flow.status.side_effect = PVOutputConnectionError + mock_pvoutput_config_flow.system.side_effect = PVOutputConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -123,7 +123,7 @@ async def test_connection_error( assert result.get("type") == RESULT_TYPE_FORM assert result.get("errors") == {"base": "cannot_connect"} - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_already_configured( @@ -175,7 +175,7 @@ async def test_import_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_reauth_flow( @@ -214,7 +214,7 @@ async def test_reauth_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_reauth_with_authentication_error( @@ -243,7 +243,7 @@ async def test_reauth_with_authentication_error( assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result - mock_pvoutput_config_flow.status.side_effect = PVOutputAuthenticationError + mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "invalid_key"}, @@ -256,9 +256,9 @@ async def test_reauth_with_authentication_error( assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 - mock_pvoutput_config_flow.status.side_effect = None + mock_pvoutput_config_flow.system.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={CONF_API_KEY: "valid_key"}, @@ -273,7 +273,7 @@ async def test_reauth_with_authentication_error( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 2 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 2 async def test_reauth_api_error( @@ -297,7 +297,7 @@ async def test_reauth_api_error( assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result - mock_pvoutput_config_flow.status.side_effect = PVOutputConnectionError + mock_pvoutput_config_flow.system.side_effect = PVOutputConnectionError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "some_new_key"}, diff --git a/tests/components/pvoutput/test_init.py b/tests/components/pvoutput/test_init.py index faaff3d4214..b583e0807e0 100644 --- a/tests/components/pvoutput/test_init.py +++ b/tests/components/pvoutput/test_init.py @@ -1,7 +1,11 @@ """Tests for the PVOutput integration.""" from unittest.mock import MagicMock -from pvo import PVOutputAuthenticationError, PVOutputConnectionError +from pvo import ( + PVOutputAuthenticationError, + PVOutputConnectionError, + PVOutputNoDataError, +) import pytest from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN @@ -35,13 +39,15 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.parametrize("side_effect", [PVOutputConnectionError, PVOutputNoDataError]) async def test_config_entry_not_ready( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pvoutput: MagicMock, + side_effect: Exception, ) -> None: """Test the PVOutput configuration entry not ready.""" - mock_pvoutput.status.side_effect = PVOutputConnectionError + mock_pvoutput.status.side_effect = side_effect mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) From 646c56e0e9d3ac45129baeeff1fd5defbecebb3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Feb 2022 19:38:55 +0100 Subject: [PATCH 255/298] Fix CPUSpeed with missing info (#66339) --- homeassistant/components/cpuspeed/sensor.py | 4 ++-- tests/components/cpuspeed/test_sensor.py | 22 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 86c02ae9ee9..99b23834549 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -81,8 +81,8 @@ class CPUSpeedSensor(SensorEntity): if info: self._attr_extra_state_attributes = { - ATTR_ARCH: info["arch_string_raw"], - ATTR_BRAND: info["brand_raw"], + ATTR_ARCH: info.get("arch_string_raw"), + ATTR_BRAND: info.get("brand_raw"), } if HZ_ADVERTISED in info: self._attr_extra_state_attributes[ATTR_HZ] = round( diff --git a/tests/components/cpuspeed/test_sensor.py b/tests/components/cpuspeed/test_sensor.py index 134d19b31ea..ebf9f0111bd 100644 --- a/tests/components/cpuspeed/test_sensor.py +++ b/tests/components/cpuspeed/test_sensor.py @@ -61,3 +61,25 @@ async def test_sensor( assert state.attributes.get(ATTR_ARCH) == "aargh" assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7" assert state.attributes.get(ATTR_HZ) == 3.6 + + +async def test_sensor_partial_info( + hass: HomeAssistant, + mock_cpuinfo: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the CPU Speed sensor missing info.""" + mock_config_entry.add_to_hass(hass) + + # Pop some info from the mocked CPUSpeed + mock_cpuinfo.return_value.pop("brand_raw") + mock_cpuinfo.return_value.pop("arch_string_raw") + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.cpu_speed") + assert state + assert state.state == "3.2" + assert state.attributes.get(ATTR_ARCH) is None + assert state.attributes.get(ATTR_BRAND) is None From c2545983318a23089eb9ae55d41cec646df53c9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Feb 2022 15:19:57 -0600 Subject: [PATCH 256/298] Add unique id to lutron caseta config entry when missing (#66346) --- homeassistant/components/lutron_caseta/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 546bb055ca8..0408f547f25 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -138,6 +138,11 @@ async def async_setup_entry( devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] + if not config_entry.unique_id: + hass.config_entries.async_update_entry( + config_entry, unique_id=hex(bridge_device["serial"])[2:].zfill(8) + ) + buttons = bridge.buttons _async_register_bridge_device(hass, entry_id, bridge_device) button_devices = _async_register_button_devices( From cb7f7dff725ff9ef8bb0775658f541af4dd7b776 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Feb 2022 13:31:16 -0800 Subject: [PATCH 257/298] Bumped version to 2022.2.6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 75cd20528bb..13250b3f9e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 7d73060f322..c912d68ed55 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.5 +version = 2022.2.6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 10c7725a901bc4328979e8aa0454e4bfa5832ce4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Feb 2022 22:53:15 +0100 Subject: [PATCH 258/298] Fix Spotify session token refresh (#66390) --- homeassistant/components/spotify/__init__.py | 6 ++++++ homeassistant/components/spotify/media_player.py | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index c3e9504c05c..446c2ace82c 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -121,6 +121,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err async def _update_devices() -> list[dict[str, Any]]: + if not session.valid_token: + await session.async_ensure_token_valid() + await hass.async_add_executor_job( + spotify.set_auth, session.token["access_token"] + ) + try: devices: dict[str, Any] | None = await hass.async_add_executor_job( spotify.devices diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index b3bb2efd1c0..06efb5558d1 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -515,9 +515,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): run_coroutine_threadsafe( self._session.async_ensure_token_valid(), self.hass.loop ).result() - self._spotify_data[DATA_SPOTIFY_CLIENT] = Spotify( - auth=self._session.token["access_token"] - ) + self._spotify.set_auth(auth=self._session.token["access_token"]) current = self._spotify.current_playback() self._currently_playing = current or {} @@ -581,7 +579,11 @@ async def async_browse_media_internal( partial(library_payload, can_play_artist=can_play_artist) ) - await session.async_ensure_token_valid() + if not session.valid_token: + await session.async_ensure_token_valid() + await hass.async_add_executor_job( + spotify.set_auth, session.token["access_token"] + ) # Strip prefix media_content_type = media_content_type[len(MEDIA_PLAYER_PREFIX) :] From 23a68f5d494f84a5a8536d0049a7a1bf6870021a Mon Sep 17 00:00:00 2001 From: uSlackr Date: Mon, 14 Feb 2022 12:17:19 -0500 Subject: [PATCH 259/298] Correct modbus address limits (#66367) --- homeassistant/components/modbus/services.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 835927e4627..87e8b98fa21 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -8,8 +8,8 @@ write_coil: required: true selector: number: - min: 1 - max: 255 + min: 0 + max: 65535 state: name: State description: State to write. @@ -42,8 +42,8 @@ write_register: required: true selector: number: - min: 1 - max: 255 + min: 0 + max: 65535 unit: name: Unit description: Address of the modbus unit. From 51dd3c88e99d3b2b214fd7e6b4f8bec008370bc8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 12 Feb 2022 22:58:35 +0100 Subject: [PATCH 260/298] Fix mesh role for Fritz old devices (#66369) --- homeassistant/components/fritz/common.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index ae506989e00..b005e536ed6 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -327,11 +327,19 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): _LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host) self._update_available, self._latest_firmware = self._update_device_info() - try: - topology = self.fritz_hosts.get_mesh_topology() - except FritzActionError: - self.mesh_role = MeshRoles.SLAVE - return + if ( + "Hosts1" not in self.connection.services + or "X_AVM-DE_GetMeshListPath" + not in self.connection.services["Hosts1"].actions + ): + self.mesh_role = MeshRoles.NONE + else: + try: + topology = self.fritz_hosts.get_mesh_topology() + except FritzActionError: + self.mesh_role = MeshRoles.SLAVE + # Avoid duplicating device trackers + return _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() From d315890c6135b14cb413104bbc87225093082222 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sat, 12 Feb 2022 14:22:21 +0000 Subject: [PATCH 261/298] Fix missing refactors of EntityCategory.XXX (#66379) * Fix missing refactors of EntityCategory.XXX * Fix entity_category refactor for homewizard --- homeassistant/components/fritz/button.py | 11 +++++------ homeassistant/components/fritzbox/binary_sensor.py | 6 +++--- homeassistant/components/goodwe/number.py | 8 ++++---- homeassistant/components/goodwe/select.py | 5 ++--- homeassistant/components/homewizard/sensor.py | 11 +++++------ homeassistant/components/onvif/button.py | 4 ++-- homeassistant/components/vicare/button.py | 4 ++-- homeassistant/components/zha/button.py | 5 +++-- homeassistant/components/zha/select.py | 5 +++-- homeassistant/components/zha/sensor.py | 3 +-- 10 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py index d17d1f4d9ef..e72af839e4c 100644 --- a/homeassistant/components/fritz/button.py +++ b/homeassistant/components/fritz/button.py @@ -12,10 +12,9 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import AvmWrapper @@ -41,28 +40,28 @@ BUTTONS: Final = [ key="firmware_update", name="Firmware Update", device_class=ButtonDeviceClass.UPDATE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda avm_wrapper: avm_wrapper.async_trigger_firmware_update(), ), FritzButtonDescription( key="reboot", name="Reboot", device_class=ButtonDeviceClass.RESTART, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda avm_wrapper: avm_wrapper.async_trigger_reboot(), ), FritzButtonDescription( key="reconnect", name="Reconnect", device_class=ButtonDeviceClass.RESTART, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda avm_wrapper: avm_wrapper.async_trigger_reconnect(), ), FritzButtonDescription( key="cleanup", name="Cleanup", icon="mdi:broom", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda avm_wrapper: avm_wrapper.async_trigger_cleanup(), ), ] diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index b58f3311cb5..b80d853e562 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -13,8 +13,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxEntity @@ -49,7 +49,7 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = ( key="lock", name="Button Lock on Device", device_class=BinarySensorDeviceClass.LOCK, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, suitable=lambda device: device.lock is not None, is_on=lambda device: not device.lock, ), @@ -57,7 +57,7 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = ( key="device_lock", name="Button Lock via UI", device_class=BinarySensorDeviceClass.LOCK, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, suitable=lambda device: device.device_lock is not None, is_on=lambda device: not device.device_lock, ), diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 06a31a4e10a..80c7885f26c 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -9,9 +9,9 @@ from goodwe import Inverter, InverterError from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG, PERCENTAGE, POWER_WATT +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER @@ -39,7 +39,7 @@ NUMBERS = ( key="grid_export_limit", name="Grid export limit", icon="mdi:transmission-tower", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, unit_of_measurement=POWER_WATT, getter=lambda inv: inv.get_grid_export_limit(), setter=lambda inv, val: inv.set_grid_export_limit(val), @@ -51,7 +51,7 @@ NUMBERS = ( key="battery_discharge_depth", name="Depth of discharge (on-grid)", icon="mdi:battery-arrow-down", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, unit_of_measurement=PERCENTAGE, getter=lambda inv: inv.get_ongrid_battery_dod(), setter=lambda inv, val: inv.set_ongrid_battery_dod(val), diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index 985c799110d..c8fa44b7e26 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -5,9 +5,8 @@ from goodwe import Inverter, InverterError from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER @@ -26,7 +25,7 @@ OPERATION_MODE = SelectEntityDescription( key="operation_mode", name="Inverter operation mode", icon="mdi:solar-power", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index c2a11386cf4..e9a09f9db86 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -16,13 +16,12 @@ from homeassistant.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_WATT, VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -37,19 +36,19 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key="smr_version", name="DSMR Version", icon="mdi:counter", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="meter_model", name="Smart Meter Model", icon="mdi:gauge", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_ssid", name="Wifi SSID", icon="mdi:wifi", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_strength", @@ -57,7 +56,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index 034573299e6..23ea5124e61 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -2,8 +2,8 @@ from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import ONVIFBaseEntity @@ -25,7 +25,7 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): """Defines a ONVIF reboot button.""" _attr_device_class = ButtonDeviceClass.RESTART - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, device: ONVIFDevice) -> None: """Initialize the button entity.""" diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index 35133b55bd1..e924a735e0f 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -14,8 +14,8 @@ import requests from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ViCareRequiredKeysMixin @@ -36,7 +36,7 @@ BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( key=BUTTON_DHW_ACTIVATE_ONETIME_CHARGE, name="Activate one-time charge", icon="mdi:shower-head", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, value_getter=lambda api: api.activateOneTimeCharge(), ), ) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 90148ba42f3..abfa94f5906 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -8,9 +8,10 @@ from typing import Any from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery @@ -96,7 +97,7 @@ class ZHAIdentifyButton(ZHAButton): return cls(unique_id, zha_device, channels, **kwargs) _attr_device_class: ButtonDeviceClass = ButtonDeviceClass.UPDATE - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _command_name = "identify" def get_args(self) -> list[Any]: diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index ff76023d96d..7cb214566d1 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -8,9 +8,10 @@ from zigpy.zcl.clusters.security import IasWd from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery @@ -46,7 +47,7 @@ async def async_setup_entry( class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): """Representation of a ZHA select entity.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _enum: Enum = None def __init__( diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index eb79634695f..1e7d4f28a38 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_VOLT_AMPERE, @@ -699,7 +698,7 @@ class RSSISensor(Sensor, id_suffix="rssi"): _state_class: SensorStateClass = SensorStateClass.MEASUREMENT _device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False @classmethod From 312c31afdac1f93abe7af5db3a196e92bef9da77 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 14 Feb 2022 02:49:19 -0500 Subject: [PATCH 262/298] revert change in vizio logic to fix bug (#66424) --- .../components/vizio/media_player.py | 22 ++++++------------- tests/components/vizio/test_media_player.py | 3 +-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index e9cd89c635a..664a8ae7da8 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -145,7 +145,7 @@ class VizioDevice(MediaPlayerEntity): self._volume_step = config_entry.options[CONF_VOLUME_STEP] self._current_input = None self._current_app_config = None - self._app_name = None + self._attr_app_name = None self._available_inputs = [] self._available_apps = [] self._all_apps = apps_coordinator.data if apps_coordinator else None @@ -209,7 +209,7 @@ class VizioDevice(MediaPlayerEntity): self._attr_volume_level = None self._attr_is_volume_muted = None self._current_input = None - self._app_name = None + self._attr_app_name = None self._current_app_config = None self._attr_sound_mode = None return @@ -265,13 +265,13 @@ class VizioDevice(MediaPlayerEntity): log_api_exception=False ) - self._app_name = find_app_name( + self._attr_app_name = find_app_name( self._current_app_config, [APP_HOME, *self._all_apps, *self._additional_app_configs], ) - if self._app_name == NO_APP_RUNNING: - self._app_name = None + if self._attr_app_name == NO_APP_RUNNING: + self._attr_app_name = None def _get_additional_app_names(self) -> list[dict[str, Any]]: """Return list of additional apps that were included in configuration.yaml.""" @@ -337,8 +337,8 @@ class VizioDevice(MediaPlayerEntity): @property def source(self) -> str | None: """Return current input of the device.""" - if self._app_name is not None and self._current_input in INPUT_APPS: - return self._app_name + if self._attr_app_name is not None and self._current_input in INPUT_APPS: + return self._attr_app_name return self._current_input @@ -364,14 +364,6 @@ class VizioDevice(MediaPlayerEntity): return self._available_inputs - @property - def app_name(self) -> str | None: - """Return the name of the current app.""" - if self.source == self._app_name: - return self._app_name - - return None - @property def app_id(self) -> str | None: """Return the ID of the current app if it is unknown by pyvizio.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index d3ef4019c57..80f72280951 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -764,6 +764,5 @@ async def test_vizio_update_with_apps_on_input( ) await _add_config_entry_to_hass(hass, config_entry) attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, STATE_ON) - # App name and app ID should not be in the attributes - assert "app_name" not in attr + # app ID should not be in the attributes assert "app_id" not in attr From 398f60c3d0d04da57c7daec5d17eef76466856d3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 13 Feb 2022 07:09:37 -0800 Subject: [PATCH 263/298] Reset the stream backoff timeout when the url updates (#66426) Reset the stream backoff timeout when the url updates, meant to improve the retry behavior for nest cameras. The problem is the nest url updates faster than the stream reset time so the wait timeout never resets if there is a temporarily problem with the new url. In particular this *may* help with the flaky cloud nest urls, but seems more correct otherwise. --- homeassistant/components/stream/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 79506c0bda2..731fd410686 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -342,7 +342,9 @@ class Stream: stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): if self._fast_restart_once: - # The stream source is updated, restart without any delay. + # The stream source is updated, restart without any delay and reset the retry + # backoff for the new url. + wait_timeout = 0 self._fast_restart_once = False self._thread_quit.clear() continue From aad9992c9abd2d23c1b3965043590260ba4023fb Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 14 Feb 2022 15:30:04 +0200 Subject: [PATCH 264/298] Increase switcher_kis timeouts (#66465) --- homeassistant/components/switcher_kis/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switcher_kis/const.py b/homeassistant/components/switcher_kis/const.py index 88b6e447446..fdd5b02fe9b 100644 --- a/homeassistant/components/switcher_kis/const.py +++ b/homeassistant/components/switcher_kis/const.py @@ -8,7 +8,7 @@ DATA_BRIDGE = "bridge" DATA_DEVICE = "device" DATA_DISCOVERY = "discovery" -DISCOVERY_TIME_SEC = 6 +DISCOVERY_TIME_SEC = 12 SIGNAL_DEVICE_ADD = "switcher_device_add" @@ -19,4 +19,4 @@ SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" # Defines the maximum interval device must send an update before it marked unavailable -MAX_UPDATE_INTERVAL_SEC = 20 +MAX_UPDATE_INTERVAL_SEC = 30 From f46fbcc9eb3d14bdba405ef544806159e3101824 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 6 Feb 2022 09:49:26 +1000 Subject: [PATCH 265/298] Bump Advantage Air to 0.2.6 (#65849) --- homeassistant/components/advantage_air/manifest.json | 10 +++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 6390ccea39c..2b72a97029b 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -3,12 +3,8 @@ "name": "Advantage Air", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/advantage_air", - "codeowners": [ - "@Bre77" - ], - "requirements": [ - "advantage_air==0.2.5" - ], + "codeowners": ["@Bre77"], + "requirements": ["advantage_air==0.2.6"], "quality_scale": "platinum", "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index b524400e811..94d82ee3e68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -114,7 +114,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.2.5 +advantage_air==0.2.6 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 358a1d21748..030043ffaaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.2.5 +advantage_air==0.2.6 # homeassistant.components.agent_dvr agent-py==0.0.23 From e5e3ab377dabd25460912ad1d97a4862fa0d0627 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 14 Feb 2022 22:16:05 +1000 Subject: [PATCH 266/298] Bump Advantage Air 0.3.0 (#66488) --- homeassistant/components/advantage_air/manifest.json | 8 ++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 2b72a97029b..56c89fe7346 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -3,8 +3,12 @@ "name": "Advantage Air", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/advantage_air", - "codeowners": ["@Bre77"], - "requirements": ["advantage_air==0.2.6"], + "codeowners": [ + "@Bre77" + ], + "requirements": [ + "advantage_air==0.3.0" + ], "quality_scale": "platinum", "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 94d82ee3e68..3d59317ac33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -114,7 +114,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.2.6 +advantage_air==0.3.0 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 030043ffaaf..52e560c6115 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.2.6 +advantage_air==0.3.0 # homeassistant.components.agent_dvr agent-py==0.0.23 From 96cbae53939e2d676e1aa3466c6c4fa70ef4943d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 15 Feb 2022 01:16:30 +0000 Subject: [PATCH 267/298] Fix utility meter restore state (#66490) * Address #63874 * avoid setting _last_period to None * name is always set in discovery * ValueError never happens only DecimalException * async_tariff_change tracks state change - state machine will not pass a None * test we only reset one utility_meter * test corrupted restored state * pretty sure _current_tariff doesn't change from init until here * missing assert * Revert "async_tariff_change tracks state change - state machine will not pass a None" This reverts commit 24fc04a964139e5cfecbfa20f91e2d30ab145d77. * address review comment * always a Decimal --- .../components/utility_meter/__init__.py | 2 -- .../components/utility_meter/sensor.py | 19 ++++++++----------- tests/components/utility_meter/test_init.py | 11 ++++++++++- tests/components/utility_meter/test_sensor.py | 11 +++++++++-- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index bf9beae060c..525b4f3b43c 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -182,8 +182,6 @@ class TariffSelect(RestoreEntity): async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() - if self._current_tariff is not None: - return state = await self.async_get_last_state() if not state or state.state not in self._tariffs: diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index b65628d5f0b..ec137968bc5 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -32,6 +32,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.template import is_number from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -166,13 +167,10 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._parent_meter = parent_meter self._sensor_source_id = source_entity self._state = None - self._last_period = 0 + self._last_period = Decimal(0) self._last_reset = dt_util.utcnow() self._collecting = None - if name: - self._name = name - else: - self._name = f"{source_entity} meter" + self._name = name self._unit_of_measurement = None self._period = meter_type if meter_type is not None: @@ -231,8 +229,6 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): return self._state += adjustment - except ValueError as err: - _LOGGER.warning("While processing state changes: %s", err) except DecimalException as err: _LOGGER.warning( "Invalid state (%s > %s): %s", old_state.state, new_state.state, err @@ -282,7 +278,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): return _LOGGER.debug("Reset utility meter <%s>", self.entity_id) self._last_reset = dt_util.utcnow() - self._last_period = str(self._state) + self._last_period = Decimal(self._state) if self._state else Decimal(0) self._state = 0 self.async_write_ha_state() @@ -319,9 +315,10 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): ATTR_UNIT_OF_MEASUREMENT ) self._last_period = ( - float(state.attributes.get(ATTR_LAST_PERIOD)) + Decimal(state.attributes[ATTR_LAST_PERIOD]) if state.attributes.get(ATTR_LAST_PERIOD) - else 0 + and is_number(state.attributes[ATTR_LAST_PERIOD]) + else Decimal(0) ) self._last_reset = dt_util.as_utc( dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET)) @@ -399,7 +396,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): state_attr = { ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, - ATTR_LAST_PERIOD: self._last_period, + ATTR_LAST_PERIOD: str(self._last_period), } if self._period is not None: state_attr[ATTR_PERIOD] = self._period diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 61e6fc4dae8..3297c696ca1 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -62,7 +62,12 @@ async def test_services(hass): "source": "sensor.energy", "cycle": "hourly", "tariffs": ["peak", "offpeak"], - } + }, + "energy_bill2": { + "source": "sensor.energy", + "cycle": "hourly", + "tariffs": ["peak", "offpeak"], + }, } } @@ -153,6 +158,10 @@ async def test_services(hass): state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "0" + # meanwhile energy_bill2_peak accumulated all kWh + state = hass.states.get("sensor.energy_bill2_peak") + assert state.state == "4" + async def test_cron(hass, legacy_patchable_time): """Test cron pattern and offset fails.""" diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 51212580aaf..fbaf795f9e2 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -304,6 +304,10 @@ async def test_restore_state(hass): ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, ), + State( + "sensor.energy_bill_midpeak", + "error", + ), State( "sensor.energy_bill_offpeak", "6", @@ -326,6 +330,9 @@ async def test_restore_state(hass): assert state.attributes.get("last_reset") == last_reset assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + state = hass.states.get("sensor.energy_bill_midpeak") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "6" assert state.attributes.get("status") == COLLECTING @@ -530,7 +537,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): assert state.attributes.get("last_reset") == now.isoformat() assert state.state == "3" else: - assert state.attributes.get("last_period") == 0 + assert state.attributes.get("last_period") == "0" assert state.state == "5" start_time_str = dt_util.parse_datetime(start_time).isoformat() assert state.attributes.get("last_reset") == start_time_str @@ -559,7 +566,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): assert state.attributes.get("last_period") == "2" assert state.state == "7" else: - assert state.attributes.get("last_period") == 0 + assert state.attributes.get("last_period") == "0" assert state.state == "9" From 5cccac7a19f02c8b72f030ea3e0fe4deb01d6b0a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 14 Feb 2022 12:43:36 +0100 Subject: [PATCH 268/298] Fix access to hass.data in hdmi-cec (#66504) Co-authored-by: epenet --- homeassistant/components/hdmi_cec/media_player.py | 8 ++++---- homeassistant/components/hdmi_cec/switch.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index c31daa85316..9ee705c1c5e 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -26,7 +26,7 @@ from pycec.const import ( from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( - DOMAIN, + DOMAIN as MP_DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, @@ -48,11 +48,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ATTR_NEW, CecEntity +from . import ATTR_NEW, DOMAIN, CecEntity _LOGGER = logging.getLogger(__name__) -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT = MP_DOMAIN + ".{}" def setup_platform( @@ -77,7 +77,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" CecEntity.__init__(self, device, logical) - self.entity_id = f"{DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" + self.entity_id = f"{MP_DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" def send_keypress(self, key): """Send keypress to CEC adapter.""" diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index 8e6deae1394..a5d64b2a7fa 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -3,17 +3,17 @@ from __future__ import annotations import logging -from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ATTR_NEW, CecEntity +from . import ATTR_NEW, DOMAIN, CecEntity _LOGGER = logging.getLogger(__name__) -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT = SWITCH_DOMAIN + ".{}" def setup_platform( @@ -38,7 +38,7 @@ class CecSwitchEntity(CecEntity, SwitchEntity): def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" CecEntity.__init__(self, device, logical) - self.entity_id = f"{DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" + self.entity_id = f"{SWITCH_DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" def turn_on(self, **kwargs) -> None: """Turn device on.""" From 6472cb8721851a09f597da7e661159b88e8917d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 15 Feb 2022 01:12:34 +0100 Subject: [PATCH 269/298] Revert "Fix raspihats callbacks (#64122)" (#66517) Co-authored-by: epenet --- .../components/raspihats/binary_sensor.py | 20 +++--------- homeassistant/components/raspihats/switch.py | 32 +++++++------------ 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py index 2c0ce10a5f3..f8fbc0d010f 100644 --- a/homeassistant/components/raspihats/binary_sensor.py +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING import voluptuous as vol @@ -109,20 +108,12 @@ class I2CHatBinarySensor(BinarySensorEntity): self._device_class = device_class self._state = self.I2C_HATS_MANAGER.read_di(self._address, self._channel) - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - if TYPE_CHECKING: - assert self.I2C_HATS_MANAGER - def online_callback(): """Call fired when board is online.""" self.schedule_update_ha_state() - await self.hass.async_add_executor_job( - self.I2C_HATS_MANAGER.register_online_callback, - self._address, - self._channel, - online_callback, + self.I2C_HATS_MANAGER.register_online_callback( + self._address, self._channel, online_callback ) def edge_callback(state): @@ -130,11 +121,8 @@ class I2CHatBinarySensor(BinarySensorEntity): self._state = state self.schedule_update_ha_state() - await self.hass.async_add_executor_job( - self.I2C_HATS_MANAGER.register_di_callback, - self._address, - self._channel, - edge_callback, + self.I2C_HATS_MANAGER.register_di_callback( + self._address, self._channel, edge_callback ) @property diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py index 0e05e376ed4..8ca88528543 100644 --- a/homeassistant/components/raspihats/switch.py +++ b/homeassistant/components/raspihats/switch.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING import voluptuous as vol @@ -101,7 +100,6 @@ class I2CHatSwitch(SwitchEntity): self._channel = channel self._name = name or DEVICE_DEFAULT_NAME self._invert_logic = invert_logic - self._state = initial_state if initial_state is not None: if self._invert_logic: state = not initial_state @@ -109,27 +107,14 @@ class I2CHatSwitch(SwitchEntity): state = initial_state self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - if TYPE_CHECKING: - assert self.I2C_HATS_MANAGER + def online_callback(): + """Call fired when board is online.""" + self.schedule_update_ha_state() - await self.hass.async_add_executor_job( - self.I2C_HATS_MANAGER.register_online_callback, - self._address, - self._channel, - self.online_callback, + self.I2C_HATS_MANAGER.register_online_callback( + self._address, self._channel, online_callback ) - def online_callback(self): - """Call fired when board is online.""" - try: - self._state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Is ON check failed, {ex!s}")) - self._state = False - self.schedule_update_ha_state() - def _log_message(self, message): """Create log message.""" string = f"{self._name} " @@ -150,7 +135,12 @@ class I2CHatSwitch(SwitchEntity): @property def is_on(self): """Return true if device is on.""" - return self._state != self._invert_logic + try: + state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) + return state != self._invert_logic + except I2CHatsException as ex: + _LOGGER.error(self._log_message(f"Is ON check failed, {ex!s}")) + return False def turn_on(self, **kwargs): """Turn the device on.""" From 104f56a01d2ddc3e451e7bca5cf65f1241894b64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Feb 2022 13:14:45 -0600 Subject: [PATCH 270/298] Fix flux_led turn on with slow responding devices (#66527) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 40661f27bab..17c73200619 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.22"], + "requirements": ["flux_led==0.28.26"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3d59317ac33..789721caa4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.22 +flux_led==0.28.26 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52e560c6115..da71e776f55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.22 +flux_led==0.28.26 # homeassistant.components.homekit fnvhash==0.1.0 From addc6bce63aac09010b257d1808abf91d3ac07dc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 15 Feb 2022 22:42:18 +0100 Subject: [PATCH 271/298] Add fallback for serialnumber (#66553) --- homeassistant/components/philips_js/__init__.py | 9 ++++++--- homeassistant/components/philips_js/config_flow.py | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 1292310f134..9a317726768 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -148,9 +148,12 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @property def unique_id(self) -> str: """Return the system descriptor.""" - assert self.config_entry - assert self.config_entry.unique_id - return self.config_entry.unique_id + entry: ConfigEntry = self.config_entry + assert entry + if entry.unique_id: + return entry.unique_id + assert entry.entry_id + return entry.entry_id @property def _notify_wanted(self): diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 89f13ffadbf..29abbe5dd71 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -122,9 +122,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - - await self.async_set_unique_id(hub.system["serialnumber"]) - self._abort_if_unique_id_configured() + if serialnumber := hub.system.get("serialnumber"): + await self.async_set_unique_id(serialnumber) + self._abort_if_unique_id_configured() self._current[CONF_SYSTEM] = hub.system self._current[CONF_API_VERSION] = hub.api_version From 8b39866bb765b5a7670b8166ffd1e6f55d95fec1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Feb 2022 15:25:36 +0100 Subject: [PATCH 272/298] Fix Tuya Covers without state in their control data point (#66564) --- homeassistant/components/tuya/cover.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 6a9e3767065..de277e61510 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -158,7 +158,10 @@ async def async_setup_entry( device = hass_data.device_manager.device_map[device_id] if descriptions := COVERS.get(device.category): for description in descriptions: - if description.key in device.status: + if ( + description.key in device.function + or description.key in device.status_range + ): entities.append( TuyaCoverEntity( device, hass_data.device_manager, description From 7bb14aae23c306da1043f88a99a999147ef2f9aa Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 15 Feb 2022 13:43:31 -0800 Subject: [PATCH 273/298] Override and disable nest stream `unavailable` behavior (#66571) --- homeassistant/components/nest/camera_sdm.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index fca79bde040..858561dd3ea 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -127,6 +127,15 @@ class NestCamera(Camera): return STREAM_TYPE_WEB_RTC return super().frontend_stream_type + @property + def available(self) -> bool: + """Return True if entity is available.""" + # Cameras are marked unavailable on stream errors in #54659 however nest streams have + # a high error rate (#60353). Given nest streams are so flaky, marking the stream + # unavailable has other side effects like not showing the camera image which sometimes + # are still able to work. Until the streams are fixed, just leave the streams as available. + return True + async def stream_source(self) -> str | None: """Return the source of the stream.""" if not self.supported_features & SUPPORT_STREAM: From de96d21ea91e9a32f545937e2cb6b085257f259e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 16 Feb 2022 02:26:13 +0100 Subject: [PATCH 274/298] Bump aiohue to version 4.1.2 (#66609) --- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/scene.py | 8 +++- homeassistant/components/hue/switch.py | 10 ++++- .../components/hue/v2/binary_sensor.py | 12 +++--- homeassistant/components/hue/v2/entity.py | 19 +++++++-- homeassistant/components/hue/v2/group.py | 4 +- homeassistant/components/hue/v2/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/conftest.py | 3 +- tests/components/hue/test_light_v2.py | 39 +++++++++++++++---- tests/components/hue/test_switch.py | 7 +++- 12 files changed, 80 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 12f7df13866..a21a3ace72b 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.0.1"], + "requirements": ["aiohue==4.1.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 3d1967ecff3..8e894b4e295 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -5,7 +5,11 @@ from typing import Any from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType -from aiohue.v2.controllers.scenes import Scene as HueScene, ScenesController +from aiohue.v2.controllers.scenes import ( + Scene as HueScene, + ScenePut as HueScenePut, + ScenesController, +) import voluptuous as vol from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity @@ -131,7 +135,7 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): await self.bridge.async_request_call( self.controller.update, self.resource.id, - HueScene(self.resource.id, speed=speed / 100), + HueScenePut(speed=speed / 100), ) await self.bridge.async_request_call( diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index 7f8e048d692..7fb40cba38f 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -5,8 +5,12 @@ from typing import Any, Union from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType -from aiohue.v2.controllers.sensors import LightLevelController, MotionController -from aiohue.v2.models.resource import SensingService +from aiohue.v2.controllers.sensors import ( + LightLevel, + LightLevelController, + Motion, + MotionController, +) from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -20,6 +24,8 @@ from .v2.entity import HueBaseEntity ControllerType = Union[LightLevelController, MotionController] +SensingService = Union[LightLevel, Motion] + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index 47617b45af6..a7077ccf765 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -4,13 +4,13 @@ from __future__ import annotations from typing import Any, Union from aiohue.v2 import HueBridgeV2 -from aiohue.v2.controllers.config import EntertainmentConfigurationController +from aiohue.v2.controllers.config import ( + EntertainmentConfiguration, + EntertainmentConfigurationController, +) from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.sensors import MotionController -from aiohue.v2.models.entertainment import ( - EntertainmentConfiguration, - EntertainmentStatus, -) +from aiohue.v2.models.entertainment_configuration import EntertainmentStatus from aiohue.v2.models.motion import Motion from homeassistant.components.binary_sensor import ( @@ -109,4 +109,4 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase): def name(self) -> str: """Return sensor name.""" type_title = self.resource.type.value.replace("_", " ").title() - return f"{self.resource.name}: {type_title}" + return f"{self.resource.metadata.name}: {type_title}" diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index c8c2f9e423b..721425606bc 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -1,11 +1,12 @@ """Generic Hue Entity Model.""" from __future__ import annotations +from typing import TYPE_CHECKING, Union + from aiohue.v2.controllers.base import BaseResourcesController from aiohue.v2.controllers.events import EventType -from aiohue.v2.models.clip import CLIPResource -from aiohue.v2.models.connectivity import ConnectivityServiceStatus from aiohue.v2.models.resource import ResourceTypes +from aiohue.v2.models.zigbee_connectivity import ConnectivityServiceStatus from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity @@ -14,6 +15,16 @@ from homeassistant.helpers.entity_registry import async_get as async_get_entity_ from ..bridge import HueBridge from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN +if TYPE_CHECKING: + from aiohue.v2.models.device_power import DevicePower + from aiohue.v2.models.grouped_light import GroupedLight + from aiohue.v2.models.light import Light + from aiohue.v2.models.light_level import LightLevel + from aiohue.v2.models.motion import Motion + + HueResource = Union[Light, DevicePower, GroupedLight, LightLevel, Motion] + + RESOURCE_TYPE_NAMES = { # a simple mapping of hue resource type to Hass name ResourceTypes.LIGHT_LEVEL: "Illuminance", @@ -30,7 +41,7 @@ class HueBaseEntity(Entity): self, bridge: HueBridge, controller: BaseResourcesController, - resource: CLIPResource, + resource: HueResource, ) -> None: """Initialize a generic Hue resource entity.""" self.bridge = bridge @@ -122,7 +133,7 @@ class HueBaseEntity(Entity): # used in subclasses @callback - def _handle_event(self, event_type: EventType, resource: CLIPResource) -> None: + def _handle_event(self, event_type: EventType, resource: HueResource) -> None: """Handle status event for this resource (or it's parent).""" if event_type == EventType.RESOURCE_DELETED and resource.id == self.resource.id: self.logger.debug("Received delete for %s", self.entity_id) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 300f08727ba..31c5a502853 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -7,7 +7,7 @@ from typing import Any from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone -from aiohue.v2.models.feature import DynamicsFeatureStatus +from aiohue.v2.models.feature import DynamicStatus from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -283,7 +283,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): total_brightness += dimming.brightness if ( light.dynamics - and light.dynamics.status == DynamicsFeatureStatus.DYNAMIC_PALETTE + and light.dynamics.status == DynamicStatus.DYNAMIC_PALETTE ): lights_in_dynamic_mode += 1 diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index ff2b7b78e7d..d331393d29b 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -12,10 +12,10 @@ from aiohue.v2.controllers.sensors import ( TemperatureController, ZigbeeConnectivityController, ) -from aiohue.v2.models.connectivity import ZigbeeConnectivity from aiohue.v2.models.device_power import DevicePower from aiohue.v2.models.light_level import LightLevel from aiohue.v2.models.temperature import Temperature +from aiohue.v2.models.zigbee_connectivity import ZigbeeConnectivity from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import ( diff --git a/requirements_all.txt b/requirements_all.txt index 789721caa4a..e94d0c1acae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.0.1 +aiohue==4.1.2 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da71e776f55..8985459f394 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.0.1 +aiohue==4.1.2 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 9e9ed9af31b..d0d15d320e0 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -8,7 +8,6 @@ from unittest.mock import AsyncMock, Mock, patch import aiohue.v1 as aiohue_v1 import aiohue.v2 as aiohue_v2 from aiohue.v2.controllers.events import EventType -from aiohue.v2.models.clip import parse_clip_resource import pytest from homeassistant.components import hue @@ -187,7 +186,7 @@ def create_mock_api_v2(hass): def emit_event(event_type, data): """Emit an event from a (hue resource) dict.""" - api.events.emit(EventType(event_type), parse_clip_resource(data)) + api.events.emit(EventType(event_type), data) api.load_test_data = load_test_data api.emit_event = emit_event diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index c7578df3a49..f0265233e4e 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -97,8 +97,12 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert mock_bridge_v2.mock_requests[0]["json"]["color_temperature"]["mirek"] == 300 # Now generate update event by emitting the json we've sent as incoming event - mock_bridge_v2.mock_requests[0]["json"]["color_temperature"].pop("mirek_valid") - mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + event = { + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "type": "light", + **mock_bridge_v2.mock_requests[0]["json"], + } + mock_bridge_v2.api.emit_event("update", event) await hass.async_block_till_done() # the light should now be on @@ -186,7 +190,12 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is False # Now generate update event by emitting the json we've sent as incoming event - mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + event = { + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "type": "light", + **mock_bridge_v2.mock_requests[0]["json"], + } + mock_bridge_v2.api.emit_event("update", event) await hass.async_block_till_done() # the light should now be off @@ -377,10 +386,20 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): ) # Now generate update events by emitting the json we've sent as incoming events - for index in range(0, 3): - mock_bridge_v2.api.emit_event( - "update", mock_bridge_v2.mock_requests[index]["json"] - ) + for index, light_id in enumerate( + [ + "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "8015b17f-8336-415b-966a-b364bd082397", + ] + ): + event = { + "id": light_id, + "type": "light", + **mock_bridge_v2.mock_requests[index]["json"], + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() await hass.async_block_till_done() # the light should now be on and have the properties we've set @@ -406,6 +425,12 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is False # Now generate update event by emitting the json we've sent as incoming event + event = { + "id": "f2416154-9607-43ab-a684-4453108a200e", + "type": "grouped_light", + **mock_bridge_v2.mock_requests[0]["json"], + } + mock_bridge_v2.api.emit_event("update", event) mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) await hass.async_block_till_done() diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index 257f1a253c3..e8086709705 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -69,7 +69,12 @@ async def test_switch_turn_off_service(hass, mock_bridge_v2, v2_resources_test_d assert mock_bridge_v2.mock_requests[0]["json"]["enabled"] is False # Now generate update event by emitting the json we've sent as incoming event - mock_bridge_v2.api.emit_event("update", mock_bridge_v2.mock_requests[0]["json"]) + event = { + "id": "b6896534-016d-4052-8cb4-ef04454df62c", + "type": "motion", + **mock_bridge_v2.mock_requests[0]["json"], + } + mock_bridge_v2.api.emit_event("update", event) await hass.async_block_till_done() # the switch should now be off From 6aa99d10638f2325671040c239dc698237e93a4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Feb 2022 17:47:46 -0800 Subject: [PATCH 275/298] Bumped version to 2022.2.7 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 13250b3f9e5..c206a4f3f7a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index c912d68ed55..a9634ea3b6f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.6 +version = 2022.2.7 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 7d2e42d026d9da542808d01beec57d6e6522838b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 15 Feb 2022 20:02:45 -0600 Subject: [PATCH 276/298] Backport #66399 to rc (#66625) --- homeassistant/components/sonos/helpers.py | 2 +- homeassistant/components/sonos/switch.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 625b54b941e..6da25314f0f 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -23,7 +23,7 @@ UID_POSTFIX = "01400" _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T", "SonosSpeaker", "SonosEntity") +_T = TypeVar("_T", bound="SonosSpeaker | SonosEntity") _R = TypeVar("_R") _P = ParamSpec("_P") diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 4e3303db45d..2ee8af61327 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime import logging +from typing import Any from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException @@ -342,20 +343,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): ATTR_INCLUDE_LINKED_ZONES: self.alarm.include_linked_zones, } - async def async_turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn alarm switch on.""" - await self.async_handle_switch_on_off(turn_on=True) + self._handle_switch_on_off(turn_on=True) - async def async_turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn alarm switch off.""" - await self.async_handle_switch_on_off(turn_on=False) + self._handle_switch_on_off(turn_on=False) - async def async_handle_switch_on_off(self, turn_on: bool) -> None: + def _handle_switch_on_off(self, turn_on: bool) -> None: """Handle turn on/off of alarm switch.""" try: _LOGGER.debug("Toggling the state of %s", self.entity_id) self.alarm.enabled = turn_on - await self.hass.async_add_executor_job(self.alarm.save) + self.alarm.save() except (OSError, SoCoException, SoCoUPnPException) as exc: _LOGGER.error("Could not update %s: %s", self.entity_id, exc) From 3168d757037e4442ced6a10e64f15bbdc7e2bb00 Mon Sep 17 00:00:00 2001 From: "Craig J. Midwinter" Date: Wed, 16 Feb 2022 12:43:02 -0600 Subject: [PATCH 277/298] Bump pysher to 1.0.7 (#59445) * Fix Goalfeed integration See https://github.com/deepbrook/Pysher/issues/62 * update requirements * Update pysher, remove websocket-client requirement for goalfeed integration --- homeassistant/components/goalfeed/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index 5b064551cf9..aed773350f6 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -2,7 +2,7 @@ "domain": "goalfeed", "name": "Goalfeed", "documentation": "https://www.home-assistant.io/integrations/goalfeed", - "requirements": ["pysher==1.0.1"], + "requirements": ["pysher==1.0.7"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index e94d0c1acae..5c903cdadf3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1831,7 +1831,7 @@ pyserial==3.5 pysesame2==1.0.1 # homeassistant.components.goalfeed -pysher==1.0.1 +pysher==1.0.7 # homeassistant.components.sia pysiaalarm==3.0.2 From e1ed15f2badf08675c1f4481f511de39b237b8df Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Feb 2022 09:24:01 -0800 Subject: [PATCH 278/298] Bump aiohue to 4.2.0 (#66670) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index a21a3ace72b..af346d1f8f6 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.1.2"], + "requirements": ["aiohue==4.2.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 5c903cdadf3..be01347cb8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.1.2 +aiohue==4.2.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8985459f394..587ec2ec374 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.1.2 +aiohue==4.2.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 From 2ad34f26894c127715f3461981e72fca3439d59d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Feb 2022 21:11:01 +0100 Subject: [PATCH 279/298] Do not pass client session to Brunt (#66671) --- homeassistant/components/brunt/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 988a96ce08e..5fe3f7d0012 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -11,7 +11,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_BAPI, DATA_COOR, DOMAIN, PLATFORMS, REGULAR_INTERVAL @@ -21,11 +20,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Brunt using config flow.""" - session = async_get_clientsession(hass) bapi = BruntClientAsync( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], - session=session, ) try: await bapi.async_login() From 54667b891a829da815caf8207e4f370ee2407db5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Feb 2022 21:11:50 +0100 Subject: [PATCH 280/298] Fix type of value in MQTT binary sensor (#66675) --- homeassistant/components/mqtt/binary_sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index aad73cd9f1a..5b95ffe8dcd 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE, + STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -105,7 +106,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" - self._state = None + self._state: bool | None = None self._expiration_trigger = None self._delay_listener = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -131,7 +132,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) return self._expired = False - self._state = last_state.state + self._state = last_state.state == STATE_ON if self._expiration_trigger: # We might have set up a trigger already after subscribing from From 75cca284483a51134821b66d06986f8ccff5a799 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Feb 2022 12:10:26 -0800 Subject: [PATCH 281/298] Cloud to avoid setting up Alexa/Google during setup phase (#66676) --- homeassistant/components/cloud/alexa_config.py | 6 +++++- homeassistant/components/cloud/google_config.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 56f49307662..11b122e2b5a 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -187,7 +187,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): self._alexa_sync_unsub = None return - if ALEXA_DOMAIN not in self.hass.config.components and self.enabled: + if ( + ALEXA_DOMAIN not in self.hass.config.components + and self.enabled + and self.hass.is_running + ): await async_setup_component(self.hass, ALEXA_DOMAIN, {}) if self.should_report_state != self.is_reporting_states: diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 4ae3b44f1fe..7988a648901 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -181,7 +181,11 @@ class CloudGoogleConfig(AbstractConfig): self.async_disable_local_sdk() return - if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + if ( + self.enabled + and GOOGLE_DOMAIN not in self.hass.config.components + and self.hass.is_running + ): await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) if self.should_report_state != self.is_reporting_state: From ab55e3f1ff16a45a4a1ceadea2390a2acb2a86a8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 16 Feb 2022 21:18:38 +0100 Subject: [PATCH 282/298] Fix last_activated timestamp on Hue scenes (#66679) --- homeassistant/components/hue/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 8e894b4e295..c21a96e4d9a 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -72,7 +72,7 @@ async def async_setup_entry( vol.Coerce(int), vol.Range(min=0, max=255) ), }, - "async_activate", + "_async_activate", ) From f5ca88b6e49cda2ca83abf6da600db1fb84ef65a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 15 Feb 2022 15:24:13 +0100 Subject: [PATCH 283/298] Add tests for samsungtv diagnostics (#66563) * Add tests for samsungtv diagnostics * Adjust coveragerc * Adjust type hints Co-authored-by: epenet --- .coveragerc | 1 - .../components/samsungtv/test_diagnostics.py | 58 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/components/samsungtv/test_diagnostics.py diff --git a/.coveragerc b/.coveragerc index 6f410b62cf1..13477927e8e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -947,7 +947,6 @@ omit = homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py homeassistant/components/samsungtv/bridge.py - homeassistant/components/samsungtv/diagnostics.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py new file mode 100644 index 00000000000..ba3d03d5702 --- /dev/null +++ b/tests/components/samsungtv/test_diagnostics.py @@ -0,0 +1,58 @@ +"""Test samsungtv diagnostics.""" +from aiohttp import ClientSession +import pytest + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.samsungtv import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.samsungtv.test_media_player import MOCK_ENTRY_WS_WITH_MAC + + +@pytest.fixture(name="config_entry") +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY_WS_WITH_MAC, + entry_id="123456", + unique_id="any", + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.mark.usefixtures("remotews") +async def test_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, hass_client: ClientSession +) -> None: + """Test config entry diagnostics.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "data": { + "host": "fake_host", + "ip_address": "test", + "mac": "aa:bb:cc:dd:ee:ff", + "method": "websocket", + "name": "fake", + "port": 8002, + "token": REDACTED, + }, + "disabled_by": None, + "domain": "samsungtv", + "entry_id": "123456", + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "title": "Mock Title", + "unique_id": "any", + "version": 2, + } + } From 9eca1c236d28c5e85950ef05e5364bd0449ba9a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 16 Feb 2022 09:29:52 +0100 Subject: [PATCH 284/298] Cleanup samsungtv tests (#66570) * Drop unused init method * Add type hints to media_player tests * Adjust test_init * Adjust media_player * Add type hints to conftest * Use Mock in test_media_player * Use lowercase in test_init * Use relative import in diagnostics * Add type hints to config_flow * Adjust coveragerc * Make gethostbyname autouse * Cleanup gethostbyname and remote fixtures * Drop unused fixtures * Undo type hints and usefixtures on media_player * Undo type hints and usefixtures in test_init * Undo type hints in conftest * Undo usefixtures in test_config_flow * Format Co-authored-by: epenet --- .coveragerc | 1 - tests/components/samsungtv/__init__.py | 13 -- tests/components/samsungtv/conftest.py | 33 ++--- .../components/samsungtv/test_config_flow.py | 134 ++++++------------ .../components/samsungtv/test_diagnostics.py | 3 +- tests/components/samsungtv/test_init.py | 49 +++---- .../components/samsungtv/test_media_player.py | 18 ++- 7 files changed, 84 insertions(+), 167 deletions(-) diff --git a/.coveragerc b/.coveragerc index 13477927e8e..a7a6298a69f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -946,7 +946,6 @@ omit = homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py - homeassistant/components/samsungtv/bridge.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 89768221665..4ad1622c6ca 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1,14 +1 @@ """Tests for the samsungtv component.""" -from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_samsungtv(hass: HomeAssistant, config: dict): - """Set up mock Samsung TV.""" - - entry = MockConfigEntry(domain=SAMSUNGTV_DOMAIN, data=config) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 05c51fdf591..f14a0f71bf1 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -5,19 +5,21 @@ import pytest import homeassistant.util.dt as dt_util -RESULT_ALREADY_CONFIGURED = "already_configured" -RESULT_ALREADY_IN_PROGRESS = "already_in_progress" + +@pytest.fixture(autouse=True) +def fake_host_fixture() -> None: + """Patch gethostbyname.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + yield @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote_class: remote = Mock() remote.__enter__ = Mock() remote.__exit__ = Mock() @@ -31,10 +33,7 @@ def remotews_fixture(): """Patch the samsungtvws SamsungTVWS.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + ) as remotews_class: remotews = Mock() remotews.__enter__ = Mock() remotews.__exit__ = Mock() @@ -59,10 +58,7 @@ def remotews_no_device_info_fixture(): """Patch the samsungtvws SamsungTVWS.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + ) as remotews_class: remotews = Mock() remotews.__enter__ = Mock() remotews.__exit__ = Mock() @@ -77,10 +73,7 @@ def remotews_soundbar_fixture(): """Patch the samsungtvws SamsungTVWS.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + ) as remotews_class: remotews = Mock() remotews.__enter__ = Mock() remotews.__exit__ = Mock() diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index c2a258b2afa..4b9c10fd654 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -43,10 +43,9 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -from tests.components.samsungtv.conftest import ( - RESULT_ALREADY_CONFIGURED, - RESULT_ALREADY_IN_PROGRESS, -) + +RESULT_ALREADY_CONFIGURED = "already_configured" +RESULT_ALREADY_IN_PROGRESS = "already_in_progress" MOCK_IMPORT_DATA = { CONF_HOST: "fake_host", @@ -232,9 +231,7 @@ async def test_user_websocket(hass: HomeAssistant, remotews: Mock): assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_user_legacy_missing_auth( - hass: HomeAssistant, remote: Mock, remotews: Mock -): +async def test_user_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -248,7 +245,7 @@ async def test_user_legacy_missing_auth( assert result["reason"] == RESULT_AUTH_MISSING -async def test_user_legacy_not_supported(hass: HomeAssistant, remote: Mock): +async def test_user_legacy_not_supported(hass: HomeAssistant): """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -262,7 +259,7 @@ async def test_user_legacy_not_supported(hass: HomeAssistant, remote: Mock): assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_websocket_not_supported(hass: HomeAssistant, remotews: Mock): +async def test_user_websocket_not_supported(hass: HomeAssistant): """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -279,7 +276,7 @@ async def test_user_websocket_not_supported(hass: HomeAssistant, remotews: Mock) assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_not_successful(hass: HomeAssistant, remotews: Mock): +async def test_user_not_successful(hass: HomeAssistant): """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -295,7 +292,7 @@ async def test_user_not_successful(hass: HomeAssistant, remotews: Mock): assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_user_not_successful_2(hass: HomeAssistant, remotews: Mock): +async def test_user_not_successful_2(hass: HomeAssistant): """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -374,9 +371,7 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock, no_mac_address: assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" -async def test_ssdp_legacy_missing_auth( - hass: HomeAssistant, remote: Mock, remotews: Mock -): +async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): """Test starting a flow from discovery with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -452,7 +447,7 @@ async def test_ssdp_websocket_success_populates_mac_address( assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_websocket_not_supported(hass: HomeAssistant, remote: Mock): +async def test_ssdp_websocket_not_supported(hass: HomeAssistant): """Test starting a flow from discovery for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -482,9 +477,7 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant, remote: Mock): assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_not_successful( - hass: HomeAssistant, remote: Mock, no_mac_address: Mock -): +async def test_ssdp_not_successful(hass: HomeAssistant, no_mac_address: Mock): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -512,9 +505,7 @@ async def test_ssdp_not_successful( assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_not_successful_2( - hass: HomeAssistant, remote: Mock, no_mac_address: Mock -): +async def test_ssdp_not_successful_2(hass: HomeAssistant, no_mac_address: Mock): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -604,15 +595,11 @@ async def test_import_legacy(hass: HomeAssistant, remote: Mock, no_mac_address: """Test importing from yaml with hostname.""" no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" - with patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_DATA, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA, + ) await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake" @@ -634,15 +621,11 @@ async def test_import_legacy_without_name( no_mac_address: Mock, ): """Test importing from yaml without a name.""" - with patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_DATA_WITHOUT_NAME, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA_WITHOUT_NAME, + ) await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake_host" @@ -658,15 +641,11 @@ async def test_import_legacy_without_name( async def test_import_websocket(hass: HomeAssistant, remotews: Mock): """Test importing from yaml with hostname.""" - with patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_WSDATA, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_WSDATA, + ) await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake" @@ -680,15 +659,11 @@ async def test_import_websocket(hass: HomeAssistant, remotews: Mock): async def test_import_websocket_without_port(hass: HomeAssistant, remotews: Mock): """Test importing from yaml with hostname by no port.""" - with patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_WSDATA, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_WSDATA, + ) await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake" @@ -817,17 +792,12 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant, remotews: Mock): assert result2["reason"] == "already_in_progress" -async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: Mock): +async def test_autodetect_websocket(hass: HomeAssistant): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews: + ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: enter = Mock() type(enter).token = PropertyMock(return_value="123456789") remote = Mock() @@ -865,14 +835,11 @@ async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -async def test_websocket_no_mac(hass: HomeAssistant, remote: Mock, remotews: Mock): +async def test_websocket_no_mac(hass: HomeAssistant): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews, patch( @@ -914,15 +881,12 @@ async def test_websocket_no_mac(hass: HomeAssistant, remote: Mock, remotews: Moc assert entries[0].data[CONF_MAC] == "gg:hh:ii:ll:mm:nn" -async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock): +async def test_autodetect_auth_missing(hass: HomeAssistant): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[AccessDenied("Boom")], - ) as remote, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + ) as remote: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) @@ -932,15 +896,12 @@ async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock): assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_not_supported(hass: HomeAssistant, remote: Mock): +async def test_autodetect_not_supported(hass: HomeAssistant): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[UnhandledResponse("Boom")], - ) as remote, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + ) as remote: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) @@ -962,7 +923,7 @@ async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): assert result["data"][CONF_PORT] == LEGACY_PORT -async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock): +async def test_autodetect_none(hass: HomeAssistant): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -970,10 +931,7 @@ async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock ) as remote, patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ) as remotews, patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + ) as remotews: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) @@ -990,7 +948,7 @@ async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock ] -async def test_update_old_entry(hass: HomeAssistant, remote: Mock, remotews: Mock): +async def test_update_old_entry(hass: HomeAssistant, remotews: Mock): """Test update of old entry.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: remote().rest_device_info.return_value = { @@ -1266,9 +1224,6 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure, - ), patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1289,7 +1244,7 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): assert result3["reason"] == "reauth_successful" -async def test_form_reauth_websocket_not_supported(hass, remotews: Mock): +async def test_form_reauth_websocket_not_supported(hass): """Test reauthenticate websocket when the device is not supported.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) @@ -1304,9 +1259,6 @@ async def test_form_reauth_websocket_not_supported(hass, remotews: Mock): with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketException, - ), patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index ba3d03d5702..990c25c8f3e 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -7,9 +7,10 @@ from homeassistant.components.samsungtv import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from .test_media_player import MOCK_ENTRY_WS_WITH_MAC + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.components.samsungtv.test_media_player import MOCK_ENTRY_WS_WITH_MAC @pytest.fixture(name="config_entry") diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index bd3e2a51256..e49b8fdc5ee 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -55,27 +55,21 @@ REMOTE_CALL = { async def test_setup(hass: HomeAssistant, remotews: Mock, no_mac_address: Mock): """Test Samsung TV integration is setup.""" - with patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) - await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) + # test name and turn_on + assert state + assert state.name == "fake_name" + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON + ) - # test name and turn_on - assert state - assert state.name == "fake_name" - assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON - ) - - # test host and port - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) + # test host and port + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): @@ -88,9 +82,6 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", return_value=None, - ), patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", ): await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() @@ -104,12 +95,8 @@ async def test_setup_from_yaml_without_port_device_online( hass: HomeAssistant, remotews: Mock ): """Test import from yaml when the device is online.""" - with patch( - "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", - ): - await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) assert len(config_entries_domain) == 1 @@ -118,13 +105,13 @@ async def test_setup_from_yaml_without_port_device_online( async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog): """Test duplicate setup of platform.""" - DUPLICATE = { + duplicate = { SAMSUNGTV_DOMAIN: [ MOCK_CONFIG[SAMSUNGTV_DOMAIN][0], MOCK_CONFIG[SAMSUNGTV_DOMAIN][0], ] } - await async_setup_component(hass, SAMSUNGTV_DOMAIN, DUPLICATE) + await async_setup_component(hass, SAMSUNGTV_DOMAIN, duplicate) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID) is None assert len(hass.states.async_all("media_player")) == 0 @@ -132,7 +119,7 @@ async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog) async def test_setup_duplicate_entries( - hass: HomeAssistant, remote: Mock, remotews: Mock, no_mac_address: Mock, caplog + hass: HomeAssistant, remote: Mock, remotews: Mock, no_mac_address: Mock ): """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index f64634acd3b..eb40e3f6d22 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -117,6 +117,9 @@ MOCK_CONFIG_NOTURNON = { ] } +# Fake mac address in all mediaplayer tests. +pytestmark = pytest.mark.usefixtures("no_mac_address") + @pytest.fixture(name="delay") def delay_fixture(): @@ -127,11 +130,6 @@ def delay_fixture(): yield delay -@pytest.fixture(autouse=True) -def mock_no_mac_address(no_mac_address): - """Fake mac address in all mediaplayer tests.""" - - async def setup_samsungtv(hass, config): """Set up mock Samsung TV.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, config) @@ -150,7 +148,7 @@ async def test_setup_without_turnon(hass, remote): assert hass.states.get(ENTITY_ID_NOTURNON) -async def test_setup_websocket(hass, remotews, mock_now): +async def test_setup_websocket(hass, remotews): """Test setup of platform.""" with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: enter = Mock() @@ -742,7 +740,7 @@ async def test_play_media(hass, remote): assert len(sleeps) == 3 -async def test_play_media_invalid_type(hass, remote): +async def test_play_media_invalid_type(hass): """Test for play_media with invalid media type.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: url = "https://example.com" @@ -764,7 +762,7 @@ async def test_play_media_invalid_type(hass, remote): assert remote.call_count == 1 -async def test_play_media_channel_as_string(hass, remote): +async def test_play_media_channel_as_string(hass): """Test for play_media with invalid channel as string.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: url = "https://example.com" @@ -786,7 +784,7 @@ async def test_play_media_channel_as_string(hass, remote): assert remote.call_count == 1 -async def test_play_media_channel_as_non_positive(hass, remote): +async def test_play_media_channel_as_non_positive(hass): """Test for play_media with invalid channel as non positive integer.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: await setup_samsungtv(hass, MOCK_CONFIG) @@ -823,7 +821,7 @@ async def test_select_source(hass, remote): assert remote.close.call_args_list == [call()] -async def test_select_source_invalid_source(hass, remote): +async def test_select_source_invalid_source(hass): """Test for select_source with invalid source.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: await setup_samsungtv(hass, MOCK_CONFIG) From d83fdae11e0c0fdfd06a7743e92eb13c915a2de4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 16 Feb 2022 21:13:11 +0100 Subject: [PATCH 285/298] Fix SamsungTVWS mocking in samsungtv tests (#66650) Co-authored-by: epenet --- tests/components/samsungtv/conftest.py | 24 +++++++++---------- .../components/samsungtv/test_config_flow.py | 17 +++++++------ .../components/samsungtv/test_media_player.py | 17 +++++++------ 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index f14a0f71bf1..14f33524f52 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -2,6 +2,8 @@ from unittest.mock import Mock, patch import pytest +from samsungctl import Remote +from samsungtvws import SamsungTVWS import homeassistant.util.dt as dt_util @@ -20,10 +22,9 @@ def fake_host_fixture() -> None: def remote_fixture(): """Patch the samsungctl Remote.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote_class: - remote = Mock() + remote = Mock(Remote) remote.__enter__ = Mock() remote.__exit__ = Mock() - remote.port.return_value = 55000 remote_class.return_value = remote yield remote @@ -34,10 +35,9 @@ def remotews_fixture(): with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews_class: - remotews = Mock() - remotews.__enter__ = Mock() + remotews = Mock(SamsungTVWS) + remotews.__enter__ = Mock(return_value=remotews) remotews.__exit__ = Mock() - remotews.port.return_value = 8002 remotews.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { @@ -48,8 +48,8 @@ def remotews_fixture(): "networkType": "wireless", }, } + remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews - remotews_class().__enter__().token = "FAKE_TOKEN" yield remotews @@ -59,12 +59,12 @@ def remotews_no_device_info_fixture(): with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews_class: - remotews = Mock() - remotews.__enter__ = Mock() + remotews = Mock(SamsungTVWS) + remotews.__enter__ = Mock(return_value=remotews) remotews.__exit__ = Mock() remotews.rest_device_info.return_value = None + remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews - remotews_class().__enter__().token = "FAKE_TOKEN" yield remotews @@ -74,8 +74,8 @@ def remotews_soundbar_fixture(): with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews_class: - remotews = Mock() - remotews.__enter__ = Mock() + remotews = Mock(SamsungTVWS) + remotews.__enter__ = Mock(return_value=remotews) remotews.__exit__ = Mock() remotews.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -87,8 +87,8 @@ def remotews_soundbar_fixture(): "type": "Samsung SoundBar", }, } + remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews - remotews_class().__enter__().token = "FAKE_TOKEN" yield remotews diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4b9c10fd654..4eedb7c2107 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,8 +1,9 @@ """Tests for Samsung TV config flow.""" import socket -from unittest.mock import Mock, PropertyMock, call, patch +from unittest.mock import Mock, call, patch from samsungctl.exceptions import AccessDenied, UnhandledResponse +from samsungtvws import SamsungTVWS from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException, WebSocketProtocolException @@ -798,10 +799,8 @@ async def test_autodetect_websocket(hass: HomeAssistant): "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: - enter = Mock() - type(enter).token = PropertyMock(return_value="123456789") - remote = Mock() - remote.__enter__ = Mock(return_value=enter) + remote = Mock(SamsungTVWS) + remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock(return_value=False) remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -815,6 +814,7 @@ async def test_autodetect_websocket(hass: HomeAssistant): "type": "Samsung SmartTV", }, } + remote.token = "123456789" remotews.return_value = remote result = await hass.config_entries.flow.async_init( @@ -845,10 +845,8 @@ async def test_websocket_no_mac(hass: HomeAssistant): ) as remotews, patch( "getmac.get_mac_address", return_value="gg:hh:ii:ll:mm:nn" ): - enter = Mock() - type(enter).token = PropertyMock(return_value="123456789") - remote = Mock() - remote.__enter__ = Mock(return_value=enter) + remote = Mock(SamsungTVWS) + remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock(return_value=False) remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -860,6 +858,7 @@ async def test_websocket_no_mac(hass: HomeAssistant): "type": "Samsung SmartTV", }, } + remote.token = "123456789" remotews.return_value = remote result = await hass.config_entries.flow.async_init( diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index eb40e3f6d22..fc51b7dbd4d 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,10 +2,11 @@ import asyncio from datetime import timedelta import logging -from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch +from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, call, patch import pytest from samsungctl import exceptions +from samsungtvws import SamsungTVWS from samsungtvws.exceptions import ConnectionFailure from websocket import WebSocketException @@ -151,10 +152,8 @@ async def test_setup_without_turnon(hass, remote): async def test_setup_websocket(hass, remotews): """Test setup of platform.""" with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: - enter = Mock() - type(enter).token = PropertyMock(return_value="987654321") - remote = Mock() - remote.__enter__ = Mock(return_value=enter) + remote = Mock(SamsungTVWS) + remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock() remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -166,6 +165,7 @@ async def test_setup_websocket(hass, remotews): "networkType": "wireless", }, } + remote.token = "987654321" remote_class.return_value = remote await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -200,10 +200,8 @@ async def test_setup_websocket_2(hass, mock_now): assert entry is config_entries[0] with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: - enter = Mock() - type(enter).token = PropertyMock(return_value="987654321") - remote = Mock() - remote.__enter__ = Mock(return_value=enter) + remote = Mock(SamsungTVWS) + remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock() remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -215,6 +213,7 @@ async def test_setup_websocket_2(hass, mock_now): "networkType": "wireless", }, } + remote.token = "987654321" remote_class.return_value = remote assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) await hass.async_block_till_done() From 69238189fb57b59e5d1b729a6fbaab2c9b1682cd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 16 Feb 2022 21:28:01 +0100 Subject: [PATCH 286/298] Fix token refresh in samsungtv (#66533) --- homeassistant/components/samsungtv/bridge.py | 7 +++++++ tests/components/samsungtv/test_media_player.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index d509da91304..d4d23a03389 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -358,6 +358,13 @@ class SamsungTVWSBridge(SamsungTVBridge): self._notify_callback() except (WebSocketException, OSError): self._remote = None + else: + if self.token != self._remote.token: + LOGGER.debug( + "SamsungTVWSBridge has provided a new token %s", + self._remote.token, + ) + self.token = self._remote.token return self._remote def stop(self) -> None: diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index fc51b7dbd4d..3f00475138e 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -165,7 +165,7 @@ async def test_setup_websocket(hass, remotews): "networkType": "wireless", }, } - remote.token = "987654321" + remote.token = "123456789" remote_class.return_value = remote await setup_samsungtv(hass, MOCK_CONFIGWS) From d977d12920191a37f318a6fed8d05450f6dfe7f2 Mon Sep 17 00:00:00 2001 From: Sascha Sander Date: Wed, 16 Feb 2022 14:33:08 +0100 Subject: [PATCH 287/298] Fix scaling of numeric Tuya values (#66644) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 15e57f223e9..c6050255901 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -41,15 +41,15 @@ class IntegerTypeData: @property def step_scaled(self) -> float: """Return the step scaled.""" - return self.scale_value(self.step) + return self.step / (10 ** self.scale) def scale_value(self, value: float | int) -> float: """Scale a value.""" - return value * 1.0 / (10 ** self.scale) + return value * self.step / (10 ** self.scale) def scale_value_back(self, value: float | int) -> int: """Return raw value for scaled.""" - return int(value * (10 ** self.scale)) + return int((value * (10 ** self.scale)) / self.step) def remap_value_to( self, @@ -82,7 +82,7 @@ class IntegerTypeData: min=int(parsed["min"]), max=int(parsed["max"]), scale=float(parsed["scale"]), - step=float(parsed["step"]), + step=max(float(parsed["step"]), 1), unit=parsed.get("unit"), type=parsed.get("type"), ) From 21b4835542cc6ddbd34cb92c93764c15191d643b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Feb 2022 17:41:43 +0100 Subject: [PATCH 288/298] Add current temp fallback in Tuya climate (#66664) --- homeassistant/components/tuya/climate.py | 8 ++++++-- homeassistant/components/tuya/const.py | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index a97a27a7453..c29c46b6bf8 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -155,8 +155,12 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): self._attr_temperature_unit = TEMP_CELSIUS # Figure out current temperature, use preferred unit or what is available - celsius_type = self.find_dpcode(DPCode.TEMP_CURRENT, dptype=DPType.INTEGER) - farhenheit_type = self.find_dpcode(DPCode.TEMP_CURRENT_F, dptype=DPType.INTEGER) + celsius_type = self.find_dpcode( + (DPCode.TEMP_CURRENT, DPCode.UPPER_TEMP), dptype=DPType.INTEGER + ) + farhenheit_type = self.find_dpcode( + (DPCode.TEMP_CURRENT_F, DPCode.UPPER_TEMP_F), dptype=DPType.INTEGER + ) if farhenheit_type and ( prefered_temperature_unit == TEMP_FAHRENHEIT or (prefered_temperature_unit == TEMP_CELSIUS and not celsius_type) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 6d6a2aa2937..4d7f9d8e166 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -345,6 +345,11 @@ class DPCode(StrEnum): TOTAL_CLEAN_COUNT = "total_clean_count" TOTAL_CLEAN_TIME = "total_clean_time" TOTAL_FORWARD_ENERGY = "total_forward_energy" + TOTAL_TIME = "total_time" + TOTAL_PM = "total_pm" + TVOC = "tvoc" + UPPER_TEMP = "upper_temp" + UPPER_TEMP_F = "upper_temp_f" UV = "uv" # UV sterilization VA_BATTERY = "va_battery" VA_HUMIDITY = "va_humidity" From 413e317c657f315bde6544e9405f46342edf6045 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Feb 2022 12:50:42 -0800 Subject: [PATCH 289/298] Bumped version to 2022.2.8 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c206a4f3f7a..87f4ff4798f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "7" +PATCH_VERSION: Final = "8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index a9634ea3b6f..b08c1185f15 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.7 +version = 2022.2.8 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From ebe7c95e4f2a47748d2af1d0caf1b76272a32db7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Feb 2022 23:07:21 +0100 Subject: [PATCH 290/298] Correct MQTT binary_sensor and sensor state restoring (#66690) --- homeassistant/components/mqtt/binary_sensor.py | 7 +++---- homeassistant/components/mqtt/sensor.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5b95ffe8dcd..5b41c916f8e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -125,6 +125,9 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): and expire_after > 0 and (last_state := await self.async_get_last_state()) is not None and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + # We might have set up a trigger already after subscribing from + # super().async_added_to_hass(), then we should not restore state + and not self._expiration_trigger ): expiration_at = last_state.last_changed + timedelta(seconds=expire_after) if expiration_at < (time_now := dt_util.utcnow()): @@ -134,10 +137,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self._expired = False self._state = last_state.state == STATE_ON - if self._expiration_trigger: - # We might have set up a trigger already after subscribing from - # super().async_added_to_hass() - self._expiration_trigger() self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at ) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 6dddf496e02..13f58a8494a 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -171,6 +171,9 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): and expire_after > 0 and (last_state := await self.async_get_last_state()) is not None and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + # We might have set up a trigger already after subscribing from + # super().async_added_to_hass(), then we should not restore state + and not self._expiration_trigger ): expiration_at = last_state.last_changed + timedelta(seconds=expire_after) if expiration_at < (time_now := dt_util.utcnow()): @@ -180,10 +183,6 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): self._expired = False self._state = last_state.state - if self._expiration_trigger: - # We might have set up a trigger already after subscribing from - # super().async_added_to_hass() - self._expiration_trigger() self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at ) From 7f9017316b3b6f718c6b94ea7a3f15e0b6179144 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Feb 2022 11:43:29 -0800 Subject: [PATCH 291/298] Bump frontend to 20220203.1 (#66827) --- 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 e29b27e9026..36f55df0932 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220203.0" + "home-assistant-frontend==20220203.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5cded6a179d..0bf79dafd6f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.6.3 hass-nabucasa==0.52.0 -home-assistant-frontend==20220203.0 +home-assistant-frontend==20220203.1 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index be01347cb8b..8f3f1214dcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220203.0 +home-assistant-frontend==20220203.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 587ec2ec374..c9f72b96c46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ hole==0.7.0 holidays==0.12 # homeassistant.components.frontend -home-assistant-frontend==20220203.0 +home-assistant-frontend==20220203.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 86aad35e77969a708a69d327860b393a913ddc1e Mon Sep 17 00:00:00 2001 From: Sascha Sander Date: Fri, 18 Feb 2022 15:00:49 +0100 Subject: [PATCH 292/298] Correct current temperature for tuya thermostats (#66715) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index c29c46b6bf8..700b974e3c9 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -353,6 +353,13 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if temperature is None: return None + if self._current_temperature.scale == 0 and self._current_temperature.step != 1: + # The current temperature can have a scale of 0 or 1 and is used for + # rounding, Home Assistant doesn't need to round but we will always + # need to divide the value by 10^1 in case of 0 as scale. + # https://developer.tuya.com/en/docs/iot/shift-temperature-scale-follow-the-setting-of-app-account-center?id=Ka9qo7so58efq#title-7-Round%20values + temperature = temperature / 10 + return self._current_temperature.scale_value(temperature) @property From 6ebf520a0c8f9e9c986a0be4d2ca4ad3c9cef758 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 17 Feb 2022 21:49:01 +0100 Subject: [PATCH 293/298] Ensure new samsungtv token is updated in the config_entry (#66731) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 14 +++++++++++ homeassistant/components/samsungtv/bridge.py | 25 +++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 212ef6c23ca..515e5c0de96 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -24,6 +24,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import run_callback_threadsafe from .bridge import ( SamsungTVBridge, @@ -117,6 +118,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Initialize bridge bridge = await _async_create_bridge_with_updated_data(hass, entry) + # Ensure new token gets saved against the config_entry + def _update_token() -> None: + """Update config entry with the new token.""" + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_TOKEN: bridge.token} + ) + + def new_token_callback() -> None: + """Update config entry with the new token.""" + run_callback_threadsafe(hass.loop, _update_token) + + bridge.register_new_token_callback(new_token_callback) + def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" bridge.stop() diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index d4d23a03389..64705435fc5 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -98,11 +98,16 @@ class SamsungTVBridge(ABC): self.host = host self.token: str | None = None self._remote: Remote | None = None - self._callback: CALLBACK_TYPE | None = None + self._reauth_callback: CALLBACK_TYPE | None = None + self._new_token_callback: CALLBACK_TYPE | None = None def register_reauth_callback(self, func: CALLBACK_TYPE) -> None: """Register a callback function.""" - self._callback = func + self._reauth_callback = func + + def register_new_token_callback(self, func: CALLBACK_TYPE) -> None: + """Register a callback function.""" + self._new_token_callback = func @abstractmethod def try_connect(self) -> str | None: @@ -176,10 +181,15 @@ class SamsungTVBridge(ABC): except OSError: LOGGER.debug("Could not establish connection") - def _notify_callback(self) -> None: + def _notify_reauth_callback(self) -> None: """Notify access denied callback.""" - if self._callback is not None: - self._callback() + if self._reauth_callback is not None: + self._reauth_callback() + + def _notify_new_token_callback(self) -> None: + """Notify new token callback.""" + if self._new_token_callback is not None: + self._new_token_callback() class SamsungTVLegacyBridge(SamsungTVBridge): @@ -245,7 +255,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except AccessDenied: - self._notify_callback() + self._notify_reauth_callback() raise except (ConnectionClosed, OSError): pass @@ -355,7 +365,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure: - self._notify_callback() + self._notify_reauth_callback() except (WebSocketException, OSError): self._remote = None else: @@ -365,6 +375,7 @@ class SamsungTVWSBridge(SamsungTVBridge): self._remote.token, ) self.token = self._remote.token + self._notify_new_token_callback() return self._remote def stop(self) -> None: From def8e933d7299796ad579a8971c756d742a5b64d Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 17 Feb 2022 15:52:06 -0500 Subject: [PATCH 294/298] Bump pyinsteon to 1.0.16 (#66759) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index e00a85a9823..3b0cdee1cc3 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.14" + "pyinsteon==1.0.16" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index 8f3f1214dcd..82bd6422be3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1582,7 +1582,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.14 +pyinsteon==1.0.16 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9f72b96c46..95fa6057b10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -987,7 +987,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.14 +pyinsteon==1.0.16 # homeassistant.components.ipma pyipma==2.0.5 From 480de899fbf3212356cc54aab3593d56daa838c5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 17 Feb 2022 21:13:09 +0200 Subject: [PATCH 295/298] Fix webostv notify service (#66760) --- homeassistant/components/webostv/notify.py | 4 ++-- tests/components/webostv/test_notify.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index df2ed7e5063..7348e978d02 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -46,8 +46,8 @@ class LgWebOSNotificationService(BaseNotificationService): if not self._client.is_connected(): await self._client.connect() - data = kwargs.get(ATTR_DATA) - icon_path = data.get(CONF_ICON, "") if data else None + data = kwargs.get(ATTR_DATA, {}) + icon_path = data.get(CONF_ICON) await self._client.send_message(message, icon_path=icon_path) except WebOsTvPairError: _LOGGER.error("Pairing with TV failed") diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index a5188545737..7e150c6eb78 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -36,6 +36,21 @@ async def test_notify(hass, client): assert client.connect.call_count == 1 client.send_message.assert_called_with(MESSAGE, icon_path=ICON_PATH) + await hass.services.async_call( + NOTIFY_DOMAIN, + TV_NAME, + { + ATTR_MESSAGE: MESSAGE, + CONF_SERVICE_DATA: { + "OTHER_DATA": "not_used", + }, + }, + blocking=True, + ) + assert client.mock_calls[0] == call.connect() + assert client.connect.call_count == 1 + client.send_message.assert_called_with(MESSAGE, icon_path=None) + async def test_notify_not_connected(hass, client, monkeypatch): """Test sending a message when client is not connected.""" From ef85afde6dc5da655ef05ab7382261815758efd1 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 18 Feb 2022 00:46:18 +0200 Subject: [PATCH 296/298] Handle default notify data in webostv (#66770) --- homeassistant/components/webostv/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index 7348e978d02..46f0086e0f6 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -46,8 +46,8 @@ class LgWebOSNotificationService(BaseNotificationService): if not self._client.is_connected(): await self._client.connect() - data = kwargs.get(ATTR_DATA, {}) - icon_path = data.get(CONF_ICON) + data = kwargs.get(ATTR_DATA) + icon_path = data.get(CONF_ICON) if data else None await self._client.send_message(message, icon_path=icon_path) except WebOsTvPairError: _LOGGER.error("Pairing with TV failed") From 474cf4be0429a4bc9e0ef8282ab5b39847ea3b55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Feb 2022 11:40:56 -0800 Subject: [PATCH 297/298] Bump aiohue to 4.2.1 (#66823) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index af346d1f8f6..318dacd3449 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.2.0"], + "requirements": ["aiohue==4.2.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 82bd6422be3..739bb68f0a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.0 +aiohue==4.2.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95fa6057b10..3def0a7de10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.6.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.0 +aiohue==4.2.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 From 15ca244c3c22f02a5a1eb4fb402e1fcc32f27c9e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Feb 2022 11:51:56 -0800 Subject: [PATCH 298/298] Bumped version to 2022.2.9 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 87f4ff4798f..1aa0b3cc35d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "8" +PATCH_VERSION: Final = "9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index b08c1185f15..9ce81a08a10 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.8 +version = 2022.2.9 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0