From 75eed17c0be494c7d5aca8c5fc076e393825fe02 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Feb 2022 21:15:32 +0100 Subject: [PATCH 001/165] Bumped version to 2022.3.0b0 --- 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 49f7ed18490..af372ebd8e8 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 = 3 -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) diff --git a/setup.cfg b/setup.cfg index 74ea6e296b5..1943703d33d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0.dev0 +version = 2022.3.0b0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From b21d954e5025422d10a04fba862fa5ca5bc6ebd1 Mon Sep 17 00:00:00 2001 From: soluga <33458264+soluga@users.noreply.github.com> Date: Thu, 24 Feb 2022 01:29:26 +0100 Subject: [PATCH 002/165] Don't try to resolve state if native_value is Null (#67134) --- homeassistant/components/wolflink/sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index f1a94cbbe20..a39b03fbd9f 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -152,10 +152,11 @@ class WolfLinkState(WolfLinkSensor): def native_value(self): """Return the state converting with supported values.""" state = super().native_value - resolved_state = [ - item for item in self.wolf_object.items if item.value == int(state) - ] - if resolved_state: - resolved_name = resolved_state[0].name - return STATES.get(resolved_name, resolved_name) + if state is not None: + resolved_state = [ + item for item in self.wolf_object.items if item.value == int(state) + ] + if resolved_state: + resolved_name = resolved_state[0].name + return STATES.get(resolved_name, resolved_name) return state From b0d043c55bbb7b2e59cf5eb372cc3990ec7edb97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 16:22:39 -0800 Subject: [PATCH 003/165] Media source to verify domain to avoid KeyError (#67137) --- .../components/media_source/__init__.py | 17 +++++++++++------ tests/components/media_source/test_init.py | 4 ++++ tests/components/netatmo/test_media_source.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index e2bd1b4903b..77b254dcf9d 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -85,11 +85,16 @@ def _get_media_item( ) -> MediaSourceItem: """Return media item.""" if media_content_id: - return MediaSourceItem.from_uri(hass, media_content_id) + item = MediaSourceItem.from_uri(hass, media_content_id) + else: + # We default to our own domain if its only one registered + domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN + return MediaSourceItem(hass, domain, "") - # We default to our own domain if its only one registered - domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN - return MediaSourceItem(hass, domain, "") + if item.domain is not None and item.domain not in hass.data[DOMAIN]: + raise ValueError("Unknown media source") + + return item @bind_hass @@ -106,7 +111,7 @@ async def async_browse_media( try: item = await _get_media_item(hass, media_content_id).async_browse() except ValueError as err: - raise BrowseError("Not a media source item") from err + raise BrowseError(str(err)) from err if content_filter is None or item.children is None: return item @@ -128,7 +133,7 @@ async def async_resolve_media(hass: HomeAssistant, media_content_id: str) -> Pla try: item = _get_media_item(hass, media_content_id) except ValueError as err: - raise Unresolvable("Not a media source item") from err + raise Unresolvable(str(err)) from err return await item.async_resolve() diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index e36ccdac931..319ef295be3 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -98,6 +98,10 @@ async def test_async_unresolve_media(hass): with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media(hass, "invalid") + # Test invalid media source + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media(hass, "media-source://media_source2") + async def test_websocket_browse_media(hass, hass_ws_client): """Test browse media websocket.""" diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index c4741672186..db1a79145b4 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -54,7 +54,7 @@ async def test_async_browse_media(hass): # Test invalid base with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") - assert str(excinfo.value) == "Not a media source item" + assert str(excinfo.value) == "Invalid media source URI" # Test successful listing media = await media_source.async_browse_media( From 3550a926295be7901893a354662ec1ab965e3926 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Feb 2022 17:42:18 -0600 Subject: [PATCH 004/165] Fix Sonos radio metadata processing with missing data (#67141) --- homeassistant/components/sonos/media.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 85d15680a97..f4108b85317 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -169,7 +169,8 @@ class SonosMedia: self.queue_size = int(queue_size) if audio_source == MUSIC_SRC_RADIO: - self.channel = et_uri_md.title + if et_uri_md: + self.channel = et_uri_md.title if ct_md and ct_md.radio_show: radio_show = ct_md.radio_show.split(",")[0] From 6a31cd92795b659f192305fd40ae3ad3c70b4dae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 16:21:24 -0800 Subject: [PATCH 005/165] Fix SQL sensor (#67144) --- homeassistant/components/sql/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 1c8e87051be..1c8514d0d26 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date +import decimal import logging import re @@ -158,7 +159,7 @@ class SQLSensor(SensorEntity): _LOGGER.debug("result = %s", res.items()) data = res[self._column_name] for key, value in res.items(): - if isinstance(value, float): + if isinstance(value, decimal.Decimal): value = float(value) if isinstance(value, date): value = value.isoformat() From 0cd4f74d739105d950c6b100e4b864464277ec44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 21:15:48 -0800 Subject: [PATCH 006/165] Allow get_states to recover (#67146) --- .../components/websocket_api/commands.py | 34 ++++++++++++++++++- .../components/websocket_api/test_commands.py | 13 +++++-- tests/components/websocket_api/test_http.py | 3 +- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 650013dda7f..4ed0a9ada96 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -34,6 +34,10 @@ from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from . import const, decorators, messages from .connection import ActiveConnection @@ -225,7 +229,35 @@ def handle_get_states( if entity_perm(state.entity_id, "read") ] - connection.send_result(msg["id"], states) + # JSON serialize here so we can recover if it blows up due to the + # state machine containing unserializable data. This command is required + # to succeed for the UI to show. + response = messages.result_message(msg["id"], states) + try: + connection.send_message(const.JSON_DUMP(response)) + return + except (ValueError, TypeError): + connection.logger.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(response, dump=const.JSON_DUMP) + ), + ) + del response + + # If we can't serialize, we'll filter out unserializable states + serialized = [] + for state in states: + try: + serialized.append(const.JSON_DUMP(state)) + except (ValueError, TypeError): + # Error is already logged above + pass + + # We now have partially serialized states. Craft some JSON. + response2 = const.JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) + response2 = response2.replace('"TO_REPLACE"', ", ".join(serialized)) + connection.send_message(response2) @decorators.websocket_command({vol.Required("type"): "get_services"}) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 130870f73f0..742d9bddd38 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -587,13 +587,20 @@ async def test_states_filters_visible(hass, hass_admin_user, websocket_client): async def test_get_states_not_allows_nan(hass, websocket_client): """Test get_states command not allows NaN floats.""" - hass.states.async_set("greeting.hello", "world", {"hello": float("NaN")}) + hass.states.async_set("greeting.hello", "world") + hass.states.async_set("greeting.bad", "data", {"hello": float("NaN")}) + hass.states.async_set("greeting.bye", "universe") await websocket_client.send_json({"id": 5, "type": "get_states"}) msg = await websocket_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_UNKNOWN_ERROR + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == [ + hass.states.get("greeting.hello").as_dict(), + hass.states.get("greeting.bye").as_dict(), + ] async def test_subscribe_unsubscribe_events_whitelist( diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 336c79d22b8..c3564d2b21b 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -76,7 +76,8 @@ async def test_non_json_message(hass, websocket_client, caplog): msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] + assert msg["success"] + assert msg["result"] == [] assert ( f"Unable to serialize to JSON. Bad data found at $.result[0](State: test_domain.entity).attributes.bad={bad_data}(" in caplog.text From f0383782f92eef3f41ffaddb070a55f77526d1f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Feb 2022 20:15:20 -1000 Subject: [PATCH 007/165] Use compact encoding for JSON websocket messages (#67148) Co-authored-by: Paulus Schoutsen --- homeassistant/components/websocket_api/commands.py | 4 +++- homeassistant/components/websocket_api/const.py | 4 +++- tests/components/websocket_api/test_messages.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 4ed0a9ada96..abc37dd2a0a 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -480,7 +480,9 @@ async def handle_subscribe_trigger( msg["id"], {"variables": variables, "context": context} ) connection.send_message( - json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False) + json.dumps( + message, cls=ExtendedJSONEncoder, allow_nan=False, separators=(",", ":") + ) ) connection.subscriptions[msg["id"]] = ( diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 9428d6fd87d..6c5615ad253 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -53,4 +53,6 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, allow_nan=False) +JSON_DUMP: Final = partial( + json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") +) diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index 3ec156e6949..618879f4b7f 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -83,13 +83,13 @@ async def test_message_to_json(caplog): json_str = message_to_json({"id": 1, "message": "xyz"}) - assert json_str == '{"id": 1, "message": "xyz"}' + assert json_str == '{"id":1,"message":"xyz"}' json_str2 = message_to_json({"id": 1, "message": _Unserializeable()}) assert ( json_str2 - == '{"id": 1, "type": "result", "success": false, "error": {"code": "unknown_error", "message": "Invalid JSON in response"}}' + == '{"id":1,"type":"result","success":false,"error":{"code":"unknown_error","message":"Invalid JSON in response"}}' ) assert "Unable to serialize to JSON" in caplog.text From f40f25473ca39d942cf9b3500c918a335420a7da Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 24 Feb 2022 00:18:14 -0500 Subject: [PATCH 008/165] Bump aiopyarr to 22.2.2 (#67149) --- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 9c43bfed282..6a9b00d2041 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.2.1"], + "requirements": ["aiopyarr==22.2.2"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 395b221ea61..9cf25c8e2f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -245,7 +245,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.2.1 +aiopyarr==22.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf79d2d991c..2d577e3d8a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.2.1 +aiopyarr==22.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From 25933e1186d72dd1b05bc30eddb3fdc75cbf344a Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Thu, 24 Feb 2022 01:05:45 -0500 Subject: [PATCH 009/165] Bump pydexcom to 0.2.3 (#67152) --- homeassistant/components/dexcom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index b60ea3a576c..25193019f7d 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,7 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.2"], + "requirements": ["pydexcom==0.2.3"], "codeowners": ["@gagebenne"], "iot_class": "cloud_polling", "loggers": ["pydexcom"] diff --git a/requirements_all.txt b/requirements_all.txt index 9cf25c8e2f5..7dfc9c19b9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1474,7 +1474,7 @@ pydeconz==87 pydelijn==1.0.0 # homeassistant.components.dexcom -pydexcom==0.2.2 +pydexcom==0.2.3 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d577e3d8a2..cc71923136d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -924,7 +924,7 @@ pydaikin==2.7.0 pydeconz==87 # homeassistant.components.dexcom -pydexcom==0.2.2 +pydexcom==0.2.3 # homeassistant.components.zwave pydispatcher==2.0.5 From 70f9196e8fca1312ed25163ee8082258fa27d774 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 24 Feb 2022 00:27:31 -0500 Subject: [PATCH 010/165] SleepIQ Dependency update (#67154) --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 48ada7b14a2..93cd1be3204 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.0.0"], + "requirements": ["asyncsleepiq==1.1.0"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 7dfc9c19b9e..9b21741348f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,7 +354,7 @@ async-upnp-client==0.23.5 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.0.0 +asyncsleepiq==1.1.0 # homeassistant.components.aten_pe atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc71923136d..93dacff8619 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -259,7 +259,7 @@ arcam-fmj==0.12.0 async-upnp-client==0.23.5 # homeassistant.components.sleepiq -asyncsleepiq==1.0.0 +asyncsleepiq==1.1.0 # homeassistant.components.aurora auroranoaa==0.0.2 From 37ebeae83bba6a2a5fa8415a30aca9bdc080d527 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 22:16:27 -0800 Subject: [PATCH 011/165] Bumped version to 2022.3.0b1 --- 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 af372ebd8e8..72968b5021d 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 = 3 -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) diff --git a/setup.cfg b/setup.cfg index 1943703d33d..3f032e34de3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b0 +version = 2022.3.0b1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 596f3110ba7cafd4def1c30a824fb88e26e016a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 18:14:38 +0100 Subject: [PATCH 012/165] Fix MQTT config entry deprecation warnings (#67174) --- homeassistant/components/mqtt/__init__.py | 77 +++++++++++------------ 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c7652fe97b9..1982d1f3df5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -179,6 +179,40 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( required=True, ) +CONFIG_SCHEMA_BASE = vol.Schema( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): vol.Any( + "auto", "1.0", "1.1", "1.2" + ), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) CONFIG_SCHEMA = vol.Schema( { @@ -191,44 +225,7 @@ CONFIG_SCHEMA = vol.Schema( cv.deprecated(CONF_TLS_VERSION), # Deprecated June 2020 cv.deprecated(CONF_USERNAME), # Deprecated in HA Core 2022.3 cv.deprecated(CONF_WILL_MESSAGE), # Deprecated in HA Core 2022.3 - vol.Schema( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional( - CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL - ): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional( - CONF_WILL_MESSAGE, default=DEFAULT_WILL - ): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional( - CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH - ): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } - ), + CONFIG_SCHEMA_BASE, ) }, extra=vol.ALLOW_EXTRA, @@ -619,7 +616,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" # If user didn't have configuration.yaml config, generate defaults if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() override = {k: entry.data[k] for k in shared_keys} @@ -811,7 +808,7 @@ class MQTT: self = hass.data[DATA_MQTT] if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) self.conf = _merge_config(entry, conf) await self.async_disconnect() From 3c0cd126dd0536c6de66ad96aa6247bd0d2529b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Feb 2022 09:03:59 -1000 Subject: [PATCH 013/165] Move camera to after deps for HomeKit (#67190) --- homeassistant/components/homekit/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 9981b3a1109..bde540d6372 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -8,8 +8,8 @@ "PyQRCode==1.2.1", "base36==0.1.1" ], - "dependencies": ["http", "camera", "ffmpeg", "network"], - "after_dependencies": ["zeroconf"], + "dependencies": ["ffmpeg", "http", "network"], + "after_dependencies": ["camera", "zeroconf"], "codeowners": ["@bdraco"], "zeroconf": ["_homekit._tcp.local."], "config_flow": true, From 32566085c818e8cc95c18fd62a9973cab4a11468 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Feb 2022 09:07:17 -1000 Subject: [PATCH 014/165] Fix ElkM1 systems that do not use password authentication (#67194) --- homeassistant/components/elkm1/__init__.py | 23 +++++++--- homeassistant/components/elkm1/config_flow.py | 24 ++++++++-- homeassistant/components/elkm1/const.py | 2 +- homeassistant/components/elkm1/discovery.py | 2 + tests/components/elkm1/test_config_flow.py | 44 +++++++++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 8b0dd26fc32..04a26f2822b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -228,7 +228,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Setting up elkm1 %s", conf["host"]) - if not entry.unique_id or ":" not in entry.unique_id and is_ip_address(host): + if (not entry.unique_id or ":" not in entry.unique_id) and is_ip_address(host): + _LOGGER.debug( + "Unique id for %s is missing during setup, trying to fill from discovery", + host, + ) if device := await async_discover_device(hass, host): async_update_entry_from_discovery(hass, entry, device) @@ -276,7 +280,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, conf[CONF_HOST] + elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME]) ): return False except asyncio.TimeoutError as exc: @@ -327,7 +331,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_wait_for_elk_to_sync( - elk: elkm1.Elk, login_timeout: int, sync_timeout: int, conf_host: str + elk: elkm1.Elk, + login_timeout: int, + sync_timeout: int, + password_auth: bool, ) -> bool: """Wait until the elk has finished sync. Can fail login or timeout.""" @@ -353,15 +360,21 @@ async def async_wait_for_elk_to_sync( success = True elk.add_handler("login", login_status) elk.add_handler("sync_complete", sync_complete) - events = ((login_event, login_timeout), (sync_event, sync_timeout)) + events = [] + if password_auth: + events.append(("login", login_event, login_timeout)) + events.append(("sync_complete", sync_event, sync_timeout)) - for event, timeout in events: + for name, event, timeout in events: + _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) try: async with async_timeout.timeout(timeout): await event.wait() except asyncio.TimeoutError: + _LOGGER.debug("Timed out waiting for %s event", name) elk.disconnect() raise + _LOGGER.debug("Received %s event", name) return success diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 2453958b3de..a21cf186005 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify +from homeassistant.util.network import is_ip_address from . import async_wait_for_elk_to_sync from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT @@ -80,7 +81,9 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, url): + if not await async_wait_for_elk_to_sync( + elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid) + ): raise InvalidAuth short_mac = _short_mac(mac) if mac else None @@ -124,6 +127,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device = ElkSystem( discovery_info.macaddress, discovery_info.ip, 0 ) + _LOGGER.debug("Elk discovered from dhcp: %s", self._discovered_device) return await self._async_handle_discovery() async def async_step_integration_discovery( @@ -135,6 +139,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info["ip_address"], discovery_info["port"], ) + _LOGGER.debug( + "Elk discovered from integration discovery: %s", self._discovered_device + ) return await self._async_handle_discovery() async def _async_handle_discovery(self) -> FlowResult: @@ -304,11 +311,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Handle import.""" - if device := await async_discover_device( - self.hass, urlparse(user_input[CONF_HOST]).hostname + _LOGGER.debug("Elk is importing from yaml") + url = _make_url_from_data(user_input) + + if self._url_already_configured(url): + return self.async_abort(reason="address_already_configured") + + host = urlparse(url).hostname + _LOGGER.debug( + "Importing is trying to fill unique id from discovery for %s", host + ) + if is_ip_address(host) and ( + device := await async_discover_device(self.hass, host) ): await self.async_set_unique_id(dr.format_mac(device.mac_address)) self._abort_if_unique_id_configured() + return (await self._async_create_or_error(user_input, True))[1] def _url_already_configured(self, url): diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 80d594fce0a..fd4856bd5d5 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_CODE, CONF_ZONE DOMAIN = "elkm1" -LOGIN_TIMEOUT = 15 +LOGIN_TIMEOUT = 20 CONF_AUTO_CONFIGURE = "auto_configure" CONF_AREA = "area" diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 7055f3958e9..326698c3686 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -29,9 +29,11 @@ def async_update_entry_from_discovery( ) -> bool: """Update a config entry from a discovery.""" if not entry.unique_id or ":" not in entry.unique_id: + _LOGGER.debug("Adding unique id from discovery: %s", device) return hass.config_entries.async_update_entry( entry, unique_id=dr.format_mac(device.mac_address) ) + _LOGGER.debug("Unique id is already present from discovery: %s", device) return False diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index d8a0feea670..49402d7b4d5 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -652,6 +652,50 @@ async def test_form_import_device_discovered(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_import_existing(hass): + """Test we abort on existing import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": f"elks://{MOCK_IP_ADDRESS}", + "username": "friend", + "password": "love", + "temperature_unit": "C", + "auto_configure": False, + "keypad": { + "enabled": True, + "exclude": [], + "include": [[1, 1], [2, 2], [3, 3]], + }, + "output": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "zone": { + "enabled": True, + "exclude": [[15, 15], [28, 208]], + "include": [], + }, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "address_already_configured" + + @pytest.mark.parametrize( "source, data", [ From 694fb2dddec3c0f128cc06f363b62285a5e36ba4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 14:54:46 -0800 Subject: [PATCH 015/165] Move media_source to after_deps (#67195) --- homeassistant/components/dlna_dms/manifest.json | 5 +++-- homeassistant/components/motioneye/manifest.json | 15 ++++----------- homeassistant/components/nest/manifest.json | 3 ++- tests/components/motioneye/test_media_source.py | 7 +++++++ tests/components/nest/test_media_source.py | 6 ++++++ 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index feee4b6e903..84cfc2e69fd 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -4,7 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "requirements": ["async-upnp-client==0.23.5"], - "dependencies": ["media_source", "ssdp"], + "dependencies": ["ssdp"], + "after_dependencies": ["media_source"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", @@ -26,4 +27,4 @@ "codeowners": ["@chishm"], "iot_class": "local_polling", "quality_scale": "platinum" -} \ No newline at end of file +} diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 0eb4dc57d9d..5c1dbb376a0 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -3,17 +3,10 @@ "name": "motionEye", "documentation": "https://www.home-assistant.io/integrations/motioneye", "config_flow": true, - "dependencies": [ - "http", - "media_source", - "webhook" - ], - "requirements": [ - "motioneye-client==0.3.12" - ], - "codeowners": [ - "@dermotduffy" - ], + "dependencies": ["http", "webhook"], + "after_dependencies": ["media_source"], + "requirements": ["motioneye-client==0.3.12"], + "codeowners": ["@dermotduffy"], "iot_class": "local_polling", "loggers": ["motioneye_client"] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 6e7ac1257fa..6968b401561 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,8 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "media_source"], + "dependencies": ["ffmpeg", "http"], + "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], "codeowners": ["@allenporter"], diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index ddc50fb7702..6c0e46b34c6 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -12,6 +12,7 @@ from homeassistant.components.media_source.models import PlayMedia from homeassistant.components.motioneye.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from . import ( TEST_CAMERA_DEVICE_IDENTIFIER, @@ -67,6 +68,12 @@ TEST_IMAGES = { _LOGGER = logging.getLogger(__name__) +@pytest.fixture(autouse=True) +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + async def test_async_browse_media_success(hass: HomeAssistant) -> None: """Test successful browse media.""" diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 015a14fb92c..2361049ecc1 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -90,6 +90,12 @@ def frame_image_data(frame_i, total_frames): return img +@pytest.fixture(autouse=True) +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + @pytest.fixture def mp4() -> io.BytesIO: """Generate test mp4 clip.""" From 9e7cbb011c196793da1d8407242e6c188bea83ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 16:30:53 -0800 Subject: [PATCH 016/165] Bump aiohue to 4.3.0 (#67202) --- 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 193d2b7fa81..bf6e7f06abd 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.1"], + "requirements": ["aiohue==4.3.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 9b21741348f..9541447aaf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.7.14 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.1 +aiohue==4.3.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93dacff8619..e244a67ffe1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.7.14 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.1 +aiohue==4.3.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 From 18087caf85e0be9d05c09ee5297fda1df59a2d9f Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 24 Feb 2022 21:28:36 -0600 Subject: [PATCH 017/165] 20220224.0 (#67204) --- homeassistant/components/frontend/manifest.json | 5 ++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ad728fd3604..f9ad2bd428d 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==20220223.0" + "home-assistant-frontend==20220224.0" ], "dependencies": [ "api", @@ -13,8 +13,7 @@ "diagnostics", "http", "lovelace", - "onboarding", - "search", + "onboarding", "search", "system_log", "websocket_api" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 176e25c414f..1bd905e6abd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.53.1 -home-assistant-frontend==20220223.0 +home-assistant-frontend==20220224.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 9541447aaf0..bf43d648d5d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -840,7 +840,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220223.0 +home-assistant-frontend==20220224.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e244a67ffe1..8484a2c12d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220223.0 +home-assistant-frontend==20220224.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 3372bdfc0f9accf6bc5bef471cf46aaf745f012d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 20:57:23 -0800 Subject: [PATCH 018/165] Bumped version to 2022.3.0b2 --- 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 72968b5021d..925c08e24fd 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 = 3 -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) diff --git a/setup.cfg b/setup.cfg index 3f032e34de3..295206ed003 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b1 +version = 2022.3.0b2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 6fcdd3b411948fefaab1d614915815d223085640 Mon Sep 17 00:00:00 2001 From: kevdliu <1766838+kevdliu@users.noreply.github.com> Date: Fri, 25 Feb 2022 03:44:17 -0500 Subject: [PATCH 019/165] Take Abode camera snapshot before fetching latest image (#67150) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/abode/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 9885ccb54ef..1eb6f859d0c 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -88,6 +88,8 @@ class AbodeCamera(AbodeDevice, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Get a camera image.""" + if not self.capture(): + return None self.refresh_image() if self._response: From b572d10e4284ab91e92dd2c33d9a09aa7c9e557f Mon Sep 17 00:00:00 2001 From: Mark Dietzer Date: Fri, 25 Feb 2022 09:15:49 -0800 Subject: [PATCH 020/165] Fix Twitch component to use new API (#67153) Co-authored-by: Paulus Schoutsen --- homeassistant/components/twitch/__init__.py | 2 +- homeassistant/components/twitch/manifest.json | 2 +- homeassistant/components/twitch/sensor.py | 112 +++++++++----- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/twitch/test_twitch.py | 138 +++++++++++------- 6 files changed, 164 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/twitch/__init__.py b/homeassistant/components/twitch/__init__.py index 0cdeb813945..64feb17d6b5 100644 --- a/homeassistant/components/twitch/__init__.py +++ b/homeassistant/components/twitch/__init__.py @@ -1 +1 @@ -"""The twitch component.""" +"""The Twitch component.""" diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 17f1c8586c0..ef68ba94518 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -2,7 +2,7 @@ "domain": "twitch", "name": "Twitch", "documentation": "https://www.home-assistant.io/integrations/twitch", - "requirements": ["python-twitch-client==0.6.0"], + "requirements": ["twitchAPI==2.5.2"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["twitch"] diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index b3357d331bd..771f88f0ef1 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -3,12 +3,18 @@ from __future__ import annotations import logging -from requests.exceptions import HTTPError -from twitch import TwitchClient +from twitchAPI.twitch import ( + AuthScope, + AuthType, + InvalidTokenException, + MissingScopeException, + Twitch, + TwitchAuthorizationException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,9 +39,12 @@ ICON = "mdi:twitch" STATE_OFFLINE = "offline" STATE_STREAMING = "streaming" +OAUTH_SCOPES = [AuthScope.USER_READ_SUBSCRIPTIONS] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_TOKEN): cv.string, } @@ -51,28 +60,45 @@ def setup_platform( """Set up the Twitch platform.""" channels = config[CONF_CHANNELS] client_id = config[CONF_CLIENT_ID] + client_secret = config[CONF_CLIENT_SECRET] oauth_token = config.get(CONF_TOKEN) - client = TwitchClient(client_id, oauth_token) + client = Twitch(app_id=client_id, app_secret=client_secret) + client.auto_refresh_auth = False try: - client.ingests.get_server_list() - except HTTPError: - _LOGGER.error("Client ID or OAuth token is not valid") + client.authenticate_app(scope=OAUTH_SCOPES) + except TwitchAuthorizationException: + _LOGGER.error("INvalid client ID or client secret") return - channel_ids = client.users.translate_usernames_to_ids(channels) + if oauth_token: + try: + client.set_user_authentication( + token=oauth_token, scope=OAUTH_SCOPES, validate=True + ) + except MissingScopeException: + _LOGGER.error("OAuth token is missing required scope") + return + except InvalidTokenException: + _LOGGER.error("OAuth token is invalid") + return - add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True) + channels = client.get_users(logins=channels) + + add_entities( + [TwitchSensor(channel=channel, client=client) for channel in channels["data"]], + True, + ) class TwitchSensor(SensorEntity): """Representation of an Twitch channel.""" - def __init__(self, channel, client): + def __init__(self, channel, client: Twitch): """Initialize the sensor.""" self._client = client self._channel = channel - self._oauth_enabled = client._oauth_token is not None + self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES) self._state = None self._preview = None self._game = None @@ -84,7 +110,7 @@ class TwitchSensor(SensorEntity): @property def name(self): """Return the name of the sensor.""" - return self._channel.display_name + return self._channel["display_name"] @property def native_value(self): @@ -101,7 +127,7 @@ class TwitchSensor(SensorEntity): """Return the state attributes.""" attr = dict(self._statistics) - if self._oauth_enabled: + if self._enable_user_auth: attr.update(self._subscription) attr.update(self._follow) @@ -112,7 +138,7 @@ class TwitchSensor(SensorEntity): @property def unique_id(self): """Return unique ID for this sensor.""" - return self._channel.id + return self._channel["id"] @property def icon(self): @@ -122,41 +148,51 @@ class TwitchSensor(SensorEntity): def update(self): """Update device state.""" - channel = self._client.channels.get_by_id(self._channel.id) + followers = self._client.get_users_follows(to_id=self._channel["id"])["total"] + channel = self._client.get_users(user_ids=[self._channel["id"]])["data"][0] self._statistics = { - ATTR_FOLLOWING: channel.followers, - ATTR_VIEWS: channel.views, + ATTR_FOLLOWING: followers, + ATTR_VIEWS: channel["view_count"], } - if self._oauth_enabled: - user = self._client.users.get() + if self._enable_user_auth: + user = self._client.get_users()["data"][0] - try: - sub = self._client.users.check_subscribed_to_channel( - user.id, self._channel.id - ) + subs = self._client.check_user_subscription( + user_id=user["id"], broadcaster_id=self._channel["id"] + ) + if "data" in subs: self._subscription = { ATTR_SUBSCRIPTION: True, - ATTR_SUBSCRIPTION_SINCE: sub.created_at, - ATTR_SUBSCRIPTION_GIFTED: sub.is_gift, + ATTR_SUBSCRIPTION_GIFTED: subs["data"][0]["is_gift"], } - except HTTPError: + elif "status" in subs and subs["status"] == 404: self._subscription = {ATTR_SUBSCRIPTION: False} - - try: - follow = self._client.users.check_follows_channel( - user.id, self._channel.id + elif "error" in subs: + raise Exception( + f"Error response on check_user_subscription: {subs['error']}" ) - self._follow = {ATTR_FOLLOW: True, ATTR_FOLLOW_SINCE: follow.created_at} - except HTTPError: + else: + raise Exception("Unknown error response on check_user_subscription") + + follows = self._client.get_users_follows( + from_id=user["id"], to_id=self._channel["id"] + )["data"] + if len(follows) > 0: + self._follow = { + ATTR_FOLLOW: True, + ATTR_FOLLOW_SINCE: follows[0]["followed_at"], + } + else: self._follow = {ATTR_FOLLOW: False} - stream = self._client.streams.get_stream_by_user(self._channel.id) - if stream: - self._game = stream.channel.get("game") - self._title = stream.channel.get("status") - self._preview = stream.preview.get("medium") + streams = self._client.get_streams(user_id=[self._channel["id"]])["data"] + if len(streams) > 0: + stream = streams[0] + self._game = stream["game_name"] + self._title = stream["title"] + self._preview = stream["thumbnail_url"] self._state = STATE_STREAMING else: - self._preview = self._channel.logo + self._preview = channel["offline_image_url"] self._state = STATE_OFFLINE diff --git a/requirements_all.txt b/requirements_all.txt index bf43d648d5d..82f12c9c57e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1987,9 +1987,6 @@ python-tado==0.12.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 -# homeassistant.components.twitch -python-twitch-client==0.6.0 - # homeassistant.components.vlc python-vlc==1.1.2 @@ -2395,6 +2392,9 @@ twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twitch +twitchAPI==2.5.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8484a2c12d3..aa09fe71623 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1239,9 +1239,6 @@ python-songpal==0.14 # homeassistant.components.tado python-tado==0.12.0 -# homeassistant.components.twitch -python-twitch-client==0.6.0 - # homeassistant.components.awair python_awair==0.2.1 @@ -1471,6 +1468,9 @@ twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twitch +twitchAPI==2.5.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 310be91c796..bfffeb4ae7f 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -1,11 +1,8 @@ """The tests for an update of the Twitch component.""" from unittest.mock import MagicMock, patch -from requests import HTTPError -from twitch.resources import Channel, Follow, Stream, Subscription, User - from homeassistant.components import sensor -from homeassistant.const import CONF_CLIENT_ID +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component ENTITY_ID = "sensor.channel123" @@ -13,6 +10,7 @@ CONFIG = { sensor.DOMAIN: { "platform": "twitch", CONF_CLIENT_ID: "1234", + CONF_CLIENT_SECRET: " abcd", "channels": ["channel123"], } } @@ -20,39 +18,46 @@ CONFIG_WITH_OAUTH = { sensor.DOMAIN: { "platform": "twitch", CONF_CLIENT_ID: "1234", + CONF_CLIENT_SECRET: "abcd", "channels": ["channel123"], "token": "9876", } } -USER_ID = User({"id": 123, "display_name": "channel123", "logo": "logo.png"}) -STREAM_OBJECT_ONLINE = Stream( - { - "channel": {"game": "Good Game", "status": "Title"}, - "preview": {"medium": "stream-medium.png"}, - } -) -CHANNEL_OBJECT = Channel({"followers": 42, "views": 24}) -OAUTH_USER_ID = User({"id": 987}) -SUB_ACTIVE = Subscription({"created_at": "2020-01-20T21:22:42", "is_gift": False}) -FOLLOW_ACTIVE = Follow({"created_at": "2020-01-20T21:22:42"}) +USER_OBJECT = { + "id": 123, + "display_name": "channel123", + "offline_image_url": "logo.png", + "view_count": 42, +} +STREAM_OBJECT_ONLINE = { + "game_name": "Good Game", + "title": "Title", + "thumbnail_url": "stream-medium.png", +} + +FOLLOWERS_OBJECT = [{"followed_at": "2020-01-20T21:22:42"}] * 24 +OAUTH_USER_ID = {"id": 987} +SUB_ACTIVE = {"is_gift": False} +FOLLOW_ACTIVE = {"followed_at": "2020-01-20T21:22:42"} + + +def make_data(data): + """Create a data object.""" + return {"data": data, "total": len(data)} async def test_init(hass): """Test initial config.""" - channels = MagicMock() - channels.get_by_id.return_value = CHANNEL_OBJECT - streams = MagicMock() - streams.get_stream_by_user.return_value = None - twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels = channels - twitch_mock.streams = streams + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True await hass.async_block_till_done() @@ -62,20 +67,21 @@ async def test_init(hass): assert sensor_state.name == "channel123" assert sensor_state.attributes["icon"] == "mdi:twitch" assert sensor_state.attributes["friendly_name"] == "channel123" - assert sensor_state.attributes["views"] == 24 - assert sensor_state.attributes["followers"] == 42 + assert sensor_state.attributes["views"] == 42 + assert sensor_state.attributes["followers"] == 24 async def test_offline(hass): """Test offline state.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock.streams.get_stream_by_user.return_value = None + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True @@ -90,12 +96,13 @@ async def test_streaming(hass): """Test streaming state.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock.streams.get_stream_by_user.return_value = STREAM_OBJECT_ONLINE + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.get_streams.return_value = make_data([STREAM_OBJECT_ONLINE]) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True @@ -112,15 +119,21 @@ async def test_oauth_without_sub_and_follow(hass): """Test state with oauth.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError() - twitch_mock.users.check_follows_channel.side_effect = HTTPError() + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([]), + ] + twitch_mock.has_required_auth.return_value = True + twitch_mock.check_user_subscription.return_value = {"status": 404} with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) @@ -135,15 +148,23 @@ async def test_oauth_with_sub(hass): """Test state with oauth and sub.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.return_value = SUB_ACTIVE - twitch_mock.users.check_follows_channel.side_effect = HTTPError() + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([]), + ] + twitch_mock.has_required_auth.return_value = True + + # This function does not return an array so use make_data + twitch_mock.check_user_subscription.return_value = make_data([SUB_ACTIVE]) with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) @@ -151,7 +172,6 @@ async def test_oauth_with_sub(hass): sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.attributes["subscribed"] is True - assert sensor_state.attributes["subscribed_since"] == "2020-01-20T21:22:42" assert sensor_state.attributes["subscription_is_gifted"] is False assert sensor_state.attributes["following"] is False @@ -160,15 +180,21 @@ async def test_oauth_with_follow(hass): """Test state with oauth and follow.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError() - twitch_mock.users.check_follows_channel.return_value = FOLLOW_ACTIVE + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([FOLLOW_ACTIVE]), + ] + twitch_mock.has_required_auth.return_value = True + twitch_mock.check_user_subscription.return_value = {"status": 404} with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) From 53632cc9e5a55c568813d88337f333abc81edeb7 Mon Sep 17 00:00:00 2001 From: martijnvanduijneveldt Date: Fri, 25 Feb 2022 17:25:13 +0100 Subject: [PATCH 021/165] Fix nanoleaf white flashing when using scenes (#67168) --- homeassistant/components/nanoleaf/light.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 29c7cb786e6..ed3476c4576 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -153,11 +153,17 @@ class NanoleafLight(NanoleafEntity, LightEntity): effect = kwargs.get(ATTR_EFFECT) transition = kwargs.get(ATTR_TRANSITION) - if hs_color: + if effect: + if effect not in self.effect_list: + raise ValueError( + f"Attempting to apply effect not in the effect list: '{effect}'" + ) + await self._nanoleaf.set_effect(effect) + elif hs_color: hue, saturation = hs_color await self._nanoleaf.set_hue(int(hue)) await self._nanoleaf.set_saturation(int(saturation)) - if color_temp_mired: + elif color_temp_mired: await self._nanoleaf.set_color_temperature( mired_to_kelvin(color_temp_mired) ) @@ -172,12 +178,6 @@ class NanoleafLight(NanoleafEntity, LightEntity): await self._nanoleaf.turn_on() if brightness: await self._nanoleaf.set_brightness(int(brightness / 2.55)) - if effect: - if effect not in self.effect_list: - raise ValueError( - f"Attempting to apply effect not in the effect list: '{effect}'" - ) - await self._nanoleaf.set_effect(effect) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" From 73eff0dde42b8e9307576460c7a1237b53884621 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 25 Feb 2022 10:27:06 -0600 Subject: [PATCH 022/165] Adjust Sonos visibility checks (#67196) --- homeassistant/components/sonos/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 71068479fe4..27d51d8f3e6 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -182,6 +182,9 @@ class SonosDiscoveryManager: soco = SoCo(ip_address) # Ensure that the player is available and UID is cached uid = soco.uid + # Abort early if the device is not visible + if not soco.is_visible: + return None _ = soco.volume return soco except NotSupportedException as exc: @@ -240,8 +243,7 @@ class SonosDiscoveryManager: None, ) if not known_uid: - soco = self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED) - if soco and soco.is_visible: + if soco := self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED): self._discovered_player(soco) self.data.hosts_heartbeat = call_later( @@ -249,8 +251,7 @@ class SonosDiscoveryManager: ) def _discovered_ip(self, ip_address): - soco = self._create_soco(ip_address, SoCoCreationSource.DISCOVERED) - if soco and soco.is_visible: + if soco := self._create_soco(ip_address, SoCoCreationSource.DISCOVERED): self._discovered_player(soco) async def _async_create_discovered_player(self, uid, discovered_ip, boot_seqnum): From 51771707fb31122382dfd26fc11a0c730092d722 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 23:40:28 -0800 Subject: [PATCH 023/165] Add media source support to Kodi (#67203) --- homeassistant/components/kodi/browse_media.py | 12 ++++++++- homeassistant/components/kodi/manifest.json | 1 + homeassistant/components/kodi/media_player.py | 26 ++++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 1b0c5d521c9..e0fdb0f73fd 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,7 +1,9 @@ """Support for media browsing.""" import asyncio +import contextlib import logging +from homeassistant.components import media_source from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, @@ -184,7 +186,7 @@ async def item_payload(item, get_thumbnail_url=None): ) -async def library_payload(): +async def library_payload(hass): """ Create response payload to describe contents of a specific library. @@ -222,6 +224,14 @@ async def library_payload(): ) ) + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media(hass, None) + # If domain is None, it's overview of available sources + if item.domain is None: + library_info.children.extend(item.children) + else: + library_info.children.append(item) + return library_info diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 3a39a7870a3..86034ea9cfc 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,6 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", + "after_dependencies": ["media_source"], "requirements": ["pykodi==0.2.7"], "codeowners": ["@OnFreund", "@cgtobi"], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 5067ee84826..56b0abb6a15 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -5,6 +5,7 @@ from datetime import timedelta from functools import wraps import logging import re +from typing import Any import urllib.parse import jsonrpc_base @@ -12,7 +13,11 @@ from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -24,6 +29,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_SEASON, MEDIA_TYPE_TRACK, MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, @@ -691,8 +697,15 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.media_seek(position) @cmd - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + media_type_lower = media_type.lower() if media_type_lower == MEDIA_TYPE_CHANNEL: @@ -700,7 +713,7 @@ class KodiEntity(MediaPlayerEntity): elif media_type_lower == MEDIA_TYPE_PLAYLIST: await self._kodi.play_playlist(int(media_id)) elif media_type_lower == "directory": - await self._kodi.play_directory(str(media_id)) + await self._kodi.play_directory(media_id) elif media_type_lower in [ MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, @@ -719,7 +732,9 @@ class KodiEntity(MediaPlayerEntity): {MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)} ) else: - await self._kodi.play_file(str(media_id)) + media_id = async_process_play_media_url(self.hass, media_id) + + await self._kodi.play_file(media_id) @cmd async def async_set_shuffle(self, shuffle): @@ -898,7 +913,10 @@ class KodiEntity(MediaPlayerEntity): ) if media_content_type in [None, "library"]: - return await library_payload() + return await library_payload(self.hass) + + if media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media(self.hass, media_content_id) payload = { "search_type": media_content_type, From 921a011391a4df667f546006f89647fe094cb663 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 25 Feb 2022 19:59:16 +1100 Subject: [PATCH 024/165] Bump the Twinkly dependency to fix the excessive debug output (#67207) --- homeassistant/components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 871cd27166d..e3b97e9385b 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -2,7 +2,7 @@ "domain": "twinkly", "name": "Twinkly", "documentation": "https://www.home-assistant.io/integrations/twinkly", - "requirements": ["ttls==1.4.2"], + "requirements": ["ttls==1.4.3"], "codeowners": ["@dr1rrb", "@Robbie1221"], "config_flow": true, "dhcp": [{ "hostname": "twinkly_*" }], diff --git a/requirements_all.txt b/requirements_all.txt index 82f12c9c57e..713cd7dd33e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,7 +2381,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.2 +ttls==1.4.3 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa09fe71623..3f94c277f31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1457,7 +1457,7 @@ total_connect_client==2022.2.1 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.2 +ttls==1.4.3 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 From 549756218bfe83fa626d2881417c915b1e91e202 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 25 Feb 2022 01:39:30 -0500 Subject: [PATCH 025/165] Don't add extra entities for zwave_js controller (#67209) * Don't add extra entities for zwave_js controller * Revert reformat of controller_state * fix indentation issues * fix indentation issues --- homeassistant/components/zwave_js/__init__.py | 22 ++-- tests/components/zwave_js/conftest.py | 14 +++ .../fixtures/aeon_smart_switch_6_state.json | 3 +- .../aeotec_radiator_thermostat_state.json | 3 +- .../fixtures/aeotec_zw164_siren_state.json | 3 +- .../fixtures/bulb_6_multi_color_state.json | 3 +- .../fixtures/chain_actuator_zws12_state.json | 3 +- .../fixtures/climate_danfoss_lc_13_state.json | 3 +- .../climate_eurotronic_spirit_z_state.json | 3 +- .../climate_heatit_z_trm2fx_state.json | 3 +- .../climate_heatit_z_trm3_no_value_state.json | 3 +- .../fixtures/climate_heatit_z_trm3_state.json | 3 +- ...setpoint_on_different_endpoints_state.json | 3 +- ..._ct100_plus_different_endpoints_state.json | 3 +- ...ate_radio_thermostat_ct100_plus_state.json | 3 +- ...ostat_ct101_multiple_temp_units_state.json | 3 +- .../fixtures/controller_node_state.json | 104 ++++++++++++++++++ .../cover_aeotec_nano_shutter_state.json | 3 +- .../fixtures/cover_fibaro_fgr222_state.json | 3 +- .../fixtures/cover_iblinds_v2_state.json | 3 +- .../fixtures/cover_qubino_shutter_state.json | 3 +- .../zwave_js/fixtures/cover_zw062_state.json | 3 +- .../fixtures/eaton_rf9640_dimmer_state.json | 3 +- .../fixtures/ecolink_door_sensor_state.json | 3 +- .../zwave_js/fixtures/fan_ge_12730_state.json | 3 +- .../zwave_js/fixtures/fan_generic_state.json | 3 +- .../zwave_js/fixtures/fan_hs_fc200_state.json | 3 +- .../fixtures/fortrezz_ssa1_siren_state.json | 3 +- .../fixtures/fortrezz_ssa3_siren_state.json | 3 +- .../ge_in_wall_dimmer_switch_state.json | 3 +- .../fixtures/hank_binary_switch_state.json | 3 +- .../fixtures/inovelli_lzw36_state.json | 3 +- .../light_color_null_values_state.json | 3 +- .../fixtures/lock_august_asl03_state.json | 3 +- .../fixtures/lock_id_lock_as_id150_state.json | 3 +- ...pp_electric_strike_lock_control_state.json | 3 +- .../fixtures/lock_schlage_be469_state.json | 3 +- .../nortek_thermostat_added_event.json | 3 +- .../fixtures/nortek_thermostat_state.json | 3 +- .../fixtures/null_name_check_state.json | 3 +- .../fixtures/srt321_hrt4_zw_state.json | 3 +- .../vision_security_zl7432_state.json | 3 +- .../wallmote_central_scene_state.json | 3 +- .../zwave_js/fixtures/zen_31_state.json | 3 +- .../fixtures/zp3111-5_not_ready_state.json | 3 +- .../zwave_js/fixtures/zp3111-5_state.json | 3 +- tests/components/zwave_js/test_button.py | 13 +++ tests/components/zwave_js/test_init.py | 2 + tests/components/zwave_js/test_sensor.py | 17 ++- 49 files changed, 247 insertions(+), 54 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/controller_node_state.json diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 5a0a7bbf29f..5d294931e66 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -293,17 +293,19 @@ async def async_setup_entry( # noqa: C901 async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" - # Create a node status sensor for each device - await async_setup_platform(SENSOR_DOMAIN) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) + # No need for a ping button or node status sensor for controller nodes + if not node.is_controller_node: + # Create a node status sensor for each device + await async_setup_platform(SENSOR_DOMAIN) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node + ) - # Create a ping button for each device - await async_setup_platform(BUTTON_DOMAIN) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node - ) + # Create a ping button for each device + await async_setup_platform(BUTTON_DOMAIN) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node + ) # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 2535daaf114..9696c922fb3 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -181,6 +181,12 @@ def controller_state_fixture(): return json.loads(load_fixture("zwave_js/controller_state.json")) +@pytest.fixture(name="controller_node_state", scope="session") +def controller_node_state_fixture(): + """Load the controller node state fixture data.""" + return json.loads(load_fixture("zwave_js/controller_node_state.json")) + + @pytest.fixture(name="version_state", scope="session") def version_state_fixture(): """Load the version state fixture data.""" @@ -535,6 +541,14 @@ def mock_client_fixture(controller_state, version_state, log_config_state): yield client +@pytest.fixture(name="controller_node") +def controller_node_fixture(client, controller_node_state): + """Mock a controller node.""" + node = Node(client, copy.deepcopy(controller_node_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="multisensor_6") def multisensor_6_fixture(client, multisensor_6_state): """Mock a multisensor 6 node.""" diff --git a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json index 1eb2baf3a02..bf547556ac8 100644 --- a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json +++ b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json @@ -1245,5 +1245,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json index 646363e3313..cbd11c66870 100644 --- a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json @@ -616,5 +616,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json index 5616abd6e0f..59e4fdfc9fb 100644 --- a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json @@ -3752,5 +3752,6 @@ } ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json index 668d1b9dce9..b0dba3c6d05 100644 --- a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json +++ b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json @@ -645,5 +645,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json index a726175fa38..2b8477b597f 100644 --- a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json +++ b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json @@ -397,5 +397,6 @@ }, "value": 0 } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index 8574674714f..206a32df664 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -432,5 +432,6 @@ "1.1" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json index afa216cac32..1241e0b35d7 100644 --- a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json @@ -712,5 +712,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json index 2526e346a53..8c655d503ed 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json @@ -1440,5 +1440,6 @@ "isSecure": false } ], - "interviewStage": "Complete" + "interviewStage": "Complete", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json index 50886b504a7..75d8bb99e55 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json @@ -1246,5 +1246,6 @@ "isSecure": true } ], - "interviewStage": "Complete" + "interviewStage": "Complete", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json index 98c185fd8d5..0ac4c6ab696 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json @@ -1173,5 +1173,6 @@ }, "value": 25.5 } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json index 52f2168fd83..8bfe3a3f7af 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json @@ -826,5 +826,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index f940dd210aa..398371a7445 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1083,5 +1083,6 @@ "version": 3, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json index 3805394dbce..b81acf66b80 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -851,5 +851,6 @@ }, "value": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json index 5feaa247f2e..5c8a12a6832 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json @@ -958,5 +958,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/controller_node_state.json b/tests/components/zwave_js/fixtures/controller_node_state.json new file mode 100644 index 00000000000..1f3c71971bc --- /dev/null +++ b/tests/components/zwave_js/fixtures/controller_node_state.json @@ -0,0 +1,104 @@ +{ + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z\u2010Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false +} diff --git a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json index b5373f38ec4..7959378a7ad 100644 --- a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json @@ -494,5 +494,6 @@ } ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json index 59dff945846..6d4defbd42c 100644 --- a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json @@ -1129,5 +1129,6 @@ "commandsDroppedRX": 1, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json index 42200c2f1d6..4d10577a2d1 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json @@ -353,5 +353,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json index bde7c90e1e4..913f24d41ae 100644 --- a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json @@ -896,5 +896,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json index a0ccd4de9c5..47aafdfd0a4 100644 --- a/tests/components/zwave_js/fixtures/cover_zw062_state.json +++ b/tests/components/zwave_js/fixtures/cover_zw062_state.json @@ -917,5 +917,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json index 3edb0494c37..a1806a99ce0 100644 --- a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json +++ b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json @@ -775,5 +775,6 @@ "value": 0, "ccVersion": 3 } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json index ac32a9f99bb..444b7eafc67 100644 --- a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json +++ b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json @@ -325,6 +325,7 @@ "2.0" ] } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json index ddbff0f3ffa..fa4c96d439a 100644 --- a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json +++ b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json @@ -427,5 +427,6 @@ "3.10" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fan_generic_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json index d09848fb759..fc89976d14a 100644 --- a/tests/components/zwave_js/fixtures/fan_generic_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -348,5 +348,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json index f83a1193c22..edab052af5b 100644 --- a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -10502,5 +10502,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 2 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json index d8973f2688e..8c88082718c 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json @@ -346,5 +346,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 2 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json index fb31f838667..aa0e05dd47f 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -351,5 +351,6 @@ "commandsDroppedTX": 0, "timeoutResponse": 1 }, - "highestSecurityClass": -1 + "highestSecurityClass": -1, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json index d47896a980a..4cbf9ef1ce4 100644 --- a/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json +++ b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json @@ -638,5 +638,6 @@ "mandatoryControlledCCs": [] }, "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/hank_binary_switch_state.json b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json index d27338db5a9..926285e5359 100644 --- a/tests/components/zwave_js/fixtures/hank_binary_switch_state.json +++ b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json @@ -720,5 +720,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json index bfa56891413..11e88eff8be 100644 --- a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json +++ b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json @@ -1952,5 +1952,6 @@ } } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json index 213b873f85c..b244913070c 100644 --- a/tests/components/zwave_js/fixtures/light_color_null_values_state.json +++ b/tests/components/zwave_js/fixtures/light_color_null_values_state.json @@ -685,5 +685,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json index b22c21e4777..642682766df 100644 --- a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json +++ b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json @@ -446,5 +446,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json index f5e66b7e7a6..5bd4cfc8080 100644 --- a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json +++ b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json @@ -2915,5 +2915,6 @@ "version": 1, "isSecure": true } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json index 2b4a3a88984..dc6e9e40d7c 100644 --- a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json +++ b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json @@ -564,5 +564,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json index fedee0f9cf1..64f83a43e0d 100644 --- a/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json +++ b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json @@ -2103,5 +2103,6 @@ }, "value": 0 } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json index 598650b863c..39c04216a04 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json @@ -250,7 +250,8 @@ "label": "Dimming duration" } } - ] + ], + "isControllerNode": false }, "result": {} } diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json index a3b34aeedf0..a99303af259 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json @@ -1275,5 +1275,6 @@ "label": "Dimming duration" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json index fe63eaee207..8905e47b155 100644 --- a/tests/components/zwave_js/fixtures/null_name_check_state.json +++ b/tests/components/zwave_js/fixtures/null_name_check_state.json @@ -410,5 +410,6 @@ "version": 3, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json index a2fdaa99561..d1db5664f76 100644 --- a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json +++ b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json @@ -258,5 +258,6 @@ "2.0" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json index d37e82ea3af..f7abbffb590 100644 --- a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json +++ b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json @@ -429,5 +429,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json index f5560dd7e78..af5314002fa 100644 --- a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json +++ b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json @@ -694,5 +694,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json index 7407607e086..3b1278da0b9 100644 --- a/tests/components/zwave_js/fixtures/zen_31_state.json +++ b/tests/components/zwave_js/fixtures/zen_31_state.json @@ -2803,5 +2803,6 @@ "version": 1, "isSecure": true } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json index f892eb5570e..272f6118830 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -64,5 +64,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index 8de7dd2b713..e652653d946 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -702,5 +702,6 @@ "commandsDroppedTX": 0, "timeoutResponse": 0 }, - "highestSecurityClass": -1 + "highestSecurityClass": -1, + "isControllerNode": false } diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 9b5ac66b06f..29858e0eb97 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -1,13 +1,16 @@ """Test the Z-Wave JS button entities.""" from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.entity_registry import async_get async def test_ping_entity( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, integration, caplog, ): @@ -44,3 +47,13 @@ async def test_ping_entity( ) assert "There is no value to refresh for this entity" in caplog.text + + # Assert a node ping button entity is not created for the controller + node = client.driver.controller.nodes[1] + assert node.is_controller_node + assert ( + async_get(hass).async_get_entity_id( + DOMAIN, "sensor", f"{get_valueless_base_unique_id(client, node)}.ping" + ) + is None + ) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 1003316f1e5..1b3a2d1204f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -934,6 +934,7 @@ async def test_replace_same_node( "commandsDroppedTX": 0, "timeoutResponse": 0, }, + "isControllerNode": False, }, "result": {}, }, @@ -1052,6 +1053,7 @@ async def test_replace_different_node( "commandsDroppedTX": 0, "timeoutResponse": 0, }, + "isControllerNode": False, }, "result": {}, }, diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 891a417551e..1d41e145a95 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.zwave_js.const import ( SERVICE_REFRESH_VALUE, SERVICE_RESET_METER, ) +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -155,7 +156,9 @@ async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration) assert entity_entry.disabled -async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integration): +async def test_node_status_sensor( + hass, client, controller_node, lock_id_lock_as_id150, integration +): """Test node status sensor is created and gets updated on node state changes.""" NODE_STATUS_ENTITY = "sensor.z_wave_module_for_id_lock_150_and_101_node_status" node = lock_id_lock_as_id150 @@ -201,6 +204,18 @@ async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integrati await client.disconnect() assert hass.states.get(NODE_STATUS_ENTITY).state != STATE_UNAVAILABLE + # Assert a node status sensor entity is not created for the controller + node = client.driver.controller.nodes[1] + assert node.is_controller_node + assert ( + ent_reg.async_get_entity_id( + DOMAIN, + "sensor", + f"{get_valueless_base_unique_id(client, node)}.node_status", + ) + is None + ) + async def test_node_status_sensor_not_ready( hass, From 2c075a00c7a9cfefbab8a47079fef85348a9d54d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 17:11:22 +0100 Subject: [PATCH 026/165] Add support for 8-gang switches to Tuya (#67218) --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/switch.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index ee653534fc8..e7340040658 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -329,6 +329,8 @@ class DPCode(StrEnum): SWITCH_4 = "switch_4" # Switch 4 SWITCH_5 = "switch_5" # Switch 5 SWITCH_6 = "switch_6" # Switch 6 + SWITCH_7 = "switch_7" # Switch 7 + SWITCH_8 = "switch_8" # Switch 8 SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch SWITCH_CHARGE = "switch_charge" SWITCH_CONTROLLER = "switch_controller" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 6be35e0102b..d978b377cc5 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -137,6 +137,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { name="Switch 6", device_class=SwitchDeviceClass.OUTLET, ), + SwitchEntityDescription( + key=DPCode.SWITCH_7, + name="Switch 7", + device_class=SwitchDeviceClass.OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_8, + name="Switch 8", + device_class=SwitchDeviceClass.OUTLET, + ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, name="USB 1", From d9195434dea394502553e90447d614536b6b4041 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 10:57:29 +0100 Subject: [PATCH 027/165] Move Phone Modem reject call deprecation warning (#67223) --- homeassistant/components/modem_callerid/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 3a1af4aa0a8..f4b2f3c3e44 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -44,13 +44,7 @@ async def async_setup_entry( ) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_REJECT_CALL, {}, "async_reject_call") - _LOGGER.warning( - "Calling reject_call service is deprecated and will be removed after 2022.4; " - "A new button entity is now available with the same function " - "and replaces the existing service" - ) class ModemCalleridSensor(SensorEntity): @@ -94,4 +88,9 @@ class ModemCalleridSensor(SensorEntity): async def async_reject_call(self) -> None: """Reject Incoming Call.""" + _LOGGER.warning( + "Calling reject_call service is deprecated and will be removed after 2022.4; " + "A new button entity is now available with the same function " + "and replaces the existing service" + ) await self.api.reject_call(self.device) From b3db4133c8037290825b8f5e8ab25e34f9d38079 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 25 Feb 2022 17:05:56 +0100 Subject: [PATCH 028/165] Fix zwave_js migration luminance sensor (#67234) --- homeassistant/components/zwave_js/migrate.py | 7 ++-- tests/components/zwave_js/test_migrate.py | 36 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 73a094fd95a..204b5d0aebd 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -9,7 +9,7 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import LIGHT_LUX, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import ( DeviceEntry, @@ -91,6 +91,8 @@ CC_ID_LABEL_TO_PROPERTY = { 113: NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME, } +UNIT_LEGACY_MIGRATION_MAP = {LIGHT_LUX: "Lux"} + class ZWaveMigrationData(TypedDict): """Represent the Z-Wave migration data dict.""" @@ -209,7 +211,8 @@ class LegacyZWaveMigration: # Normalize unit of measurement. if unit := entity_entry.unit_of_measurement: - unit = unit.lower() + _unit = UNIT_LEGACY_MIGRATION_MAP.get(unit, unit) + unit = _unit.lower() if unit == "": unit = None diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index 3479638b387..95f969a9586 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -8,6 +8,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.zwave_js.api import ENTRY_ID, ID, TYPE from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.const import LIGHT_LUX from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR @@ -33,6 +34,10 @@ ZWAVE_MULTISENSOR_DEVICE_NAME = "Z-Wave Multisensor Device" ZWAVE_MULTISENSOR_DEVICE_AREA = "Z-Wave Multisensor Area" ZWAVE_SOURCE_NODE_ENTITY = "sensor.zwave_source_node" ZWAVE_SOURCE_NODE_UNIQUE_ID = "52-4321" +ZWAVE_LUMINANCE_ENTITY = "sensor.zwave_luminance" +ZWAVE_LUMINANCE_UNIQUE_ID = "52-6543" +ZWAVE_LUMINANCE_NAME = "Z-Wave Luminance" +ZWAVE_LUMINANCE_ICON = "mdi:zwave-test-luminance" ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level" ZWAVE_BATTERY_UNIQUE_ID = "52-1234" ZWAVE_BATTERY_NAME = "Z-Wave Battery Level" @@ -69,6 +74,14 @@ def zwave_migration_data_fixture(hass): platform="zwave", name="Z-Wave Source Node", ) + zwave_luminance_entry = er.RegistryEntry( + entity_id=ZWAVE_LUMINANCE_ENTITY, + unique_id=ZWAVE_LUMINANCE_UNIQUE_ID, + platform="zwave", + name=ZWAVE_LUMINANCE_NAME, + icon=ZWAVE_LUMINANCE_ICON, + unit_of_measurement="lux", + ) zwave_battery_entry = er.RegistryEntry( entity_id=ZWAVE_BATTERY_ENTITY, unique_id=ZWAVE_BATTERY_UNIQUE_ID, @@ -131,6 +144,18 @@ def zwave_migration_data_fixture(hass): "unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID, "unit_of_measurement": zwave_source_node_entry.unit_of_measurement, }, + ZWAVE_LUMINANCE_ENTITY: { + "node_id": 52, + "node_instance": 1, + "command_class": 49, + "command_class_label": "Luminance", + "value_index": 3, + "device_id": zwave_multisensor_device.id, + "domain": zwave_luminance_entry.domain, + "entity_id": zwave_luminance_entry.entity_id, + "unique_id": ZWAVE_LUMINANCE_UNIQUE_ID, + "unit_of_measurement": zwave_luminance_entry.unit_of_measurement, + }, ZWAVE_BATTERY_ENTITY: { "node_id": 52, "node_instance": 1, @@ -169,6 +194,7 @@ def zwave_migration_data_fixture(hass): { ZWAVE_SWITCH_ENTITY: zwave_switch_entry, ZWAVE_SOURCE_NODE_ENTITY: zwave_source_node_entry, + ZWAVE_LUMINANCE_ENTITY: zwave_luminance_entry, ZWAVE_BATTERY_ENTITY: zwave_battery_entry, ZWAVE_POWER_ENTITY: zwave_power_entry, ZWAVE_TAMPERING_ENTITY: zwave_tampering_entry, @@ -218,6 +244,7 @@ async def test_migrate_zwave( migration_entity_map = { ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6", + ZWAVE_LUMINANCE_ENTITY: "sensor.multisensor_6_illuminance", ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level", } @@ -225,6 +252,7 @@ async def test_migrate_zwave( ZWAVE_SWITCH_ENTITY, ZWAVE_POWER_ENTITY, ZWAVE_SOURCE_NODE_ENTITY, + ZWAVE_LUMINANCE_ENTITY, ZWAVE_BATTERY_ENTITY, ZWAVE_TAMPERING_ENTITY, ] @@ -279,6 +307,7 @@ async def test_migrate_zwave( # this should have been migrated and no longer present under that id assert not ent_reg.async_is_registered("sensor.multisensor_6_battery_level") + assert not ent_reg.async_is_registered("sensor.multisensor_6_illuminance") # these should not have been migrated and is still in the registry assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY) @@ -295,6 +324,7 @@ async def test_migrate_zwave( # this is the new entity_ids of the zwave_js entities assert ent_reg.async_is_registered(ZWAVE_SWITCH_ENTITY) assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + assert ent_reg.async_is_registered(ZWAVE_LUMINANCE_ENTITY) # check that the migrated entries have correct attributes switch_entry = ent_reg.async_get(ZWAVE_SWITCH_ENTITY) @@ -307,6 +337,12 @@ async def test_migrate_zwave( assert battery_entry.unique_id == "3245146787.52-128-0-level" assert battery_entry.name == ZWAVE_BATTERY_NAME assert battery_entry.icon == ZWAVE_BATTERY_ICON + luminance_entry = ent_reg.async_get(ZWAVE_LUMINANCE_ENTITY) + assert luminance_entry + assert luminance_entry.unique_id == "3245146787.52-49-0-Illuminance" + assert luminance_entry.name == ZWAVE_LUMINANCE_NAME + assert luminance_entry.icon == ZWAVE_LUMINANCE_ICON + assert luminance_entry.unit_of_measurement == LIGHT_LUX # check that the zwave config entry has been removed assert not hass.config_entries.async_entries("zwave") From b767f83dc6fafa19ffc3fd722f8fdcbec5f892e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 10:00:03 -0800 Subject: [PATCH 029/165] Adjust serializing resolved media (#67240) --- .../components/media_player/browse_media.py | 8 +++--- .../components/media_source/__init__.py | 26 +++++++++---------- tests/components/media_source/test_init.py | 3 ++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 6fe4683c1fc..26494e4c8a7 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -15,7 +15,9 @@ from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY @callback -def async_process_play_media_url(hass: HomeAssistant, media_content_id: str) -> str: +def async_process_play_media_url( + hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False +) -> str: """Update a media URL with authentication if it points at Home Assistant.""" if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): return media_content_id @@ -34,8 +36,8 @@ def async_process_play_media_url(hass: HomeAssistant, media_content_id: str) -> ) media_content_id = str(parsed.join(yarl.URL(signed_path))) - # prepend external URL - if media_content_id[0] == "/": + # convert relative URL to absolute URL + if media_content_id[0] == "/" and not allow_relative_url: media_content_id = f"{get_url(hass)}{media_content_id}" return media_content_id diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 77b254dcf9d..2bcd80a39ab 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -2,21 +2,20 @@ from __future__ import annotations from collections.abc import Callable -import dataclasses -from datetime import timedelta from typing import Any -from urllib.parse import quote import voluptuous as vol from homeassistant.components import frontend, websocket_api -from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, CONTENT_AUTH_EXPIRY_TIME, BrowseError, BrowseMedia, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( @@ -177,13 +176,12 @@ async def websocket_resolve_media( connection.send_error(msg["id"], "resolve_media_failed", str(err)) return - data = dataclasses.asdict(media) - - if data["url"][0] == "/": - data["url"] = async_sign_path( - hass, - quote(data["url"]), - timedelta(seconds=msg["expires"]), - ) - - connection.send_result(msg["id"], data) + connection.send_result( + msg["id"], + { + "url": async_process_play_media_url( + hass, media.url, allow_relative_url=True + ), + "mime_type": media.mime_type, + }, + ) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 319ef295be3..2655000efc9 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -187,7 +187,8 @@ async def test_websocket_resolve_media(hass, hass_ws_client, filename): assert msg["id"] == 1 assert msg["result"]["mime_type"] == media.mime_type - # Validate url is signed. + # Validate url is relative and signed. + assert msg["result"]["url"][0] == "/" parsed = yarl.URL(msg["result"]["url"]) assert parsed.path == getattr(media, "url") assert "authSig" in parsed.query From a7c67e6cde7433cb6afd764044adb709acc4a008 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 10:01:16 -0800 Subject: [PATCH 030/165] Bumped version to 2022.3.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 925c08e24fd..95f2b3d9f57 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 = 3 -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 295206ed003..00a0d4a695f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b2 +version = 2022.3.0b3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 33969fd4c1dc3866a8130dc7d6ab746a8952cbf1 Mon Sep 17 00:00:00 2001 From: stegm Date: Sat, 26 Feb 2022 22:32:38 +0100 Subject: [PATCH 031/165] Add diagnostics to Kostal Plenticore (#66435) --- .../kostal_plenticore/diagnostics.py | 42 ++++++++ .../components/kostal_plenticore/conftest.py | 96 +++++++++++++++++++ .../kostal_plenticore/test_diagnostics.py | 49 ++++++++++ 3 files changed, 187 insertions(+) create mode 100644 homeassistant/components/kostal_plenticore/diagnostics.py create mode 100644 tests/components/kostal_plenticore/conftest.py create mode 100644 tests/components/kostal_plenticore/test_diagnostics.py diff --git a/homeassistant/components/kostal_plenticore/diagnostics.py b/homeassistant/components/kostal_plenticore/diagnostics.py new file mode 100644 index 00000000000..2e061d35528 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/diagnostics.py @@ -0,0 +1,42 @@ +"""Diagnostics support for Kostal Plenticore.""" +from __future__ import annotations + +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 +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .helper import Plenticore + +TO_REDACT = {CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, dict[str, Any]]: + """Return diagnostics for a config entry.""" + data = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)} + + plenticore: Plenticore = hass.data[DOMAIN][config_entry.entry_id] + + # Get information from Kostal Plenticore library + available_process_data = await plenticore.client.get_process_data() + available_settings_data = await plenticore.client.get_settings() + data["client"] = { + "version": str(await plenticore.client.get_version()), + "me": str(await plenticore.client.get_me()), + "available_process_data": available_process_data, + "available_settings_data": { + module_id: [str(setting) for setting in settings] + for module_id, settings in available_settings_data.items() + }, + } + + device_info = {**plenticore.device_info} + device_info["identifiers"] = REDACTED # contains serial number + data["device"] = device_info + + return data diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py new file mode 100644 index 00000000000..c3ed1b45592 --- /dev/null +++ b/tests/components/kostal_plenticore/conftest.py @@ -0,0 +1,96 @@ +"""Fixtures for Kostal Plenticore tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from kostal.plenticore import MeData, SettingsData, VersionData +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, +) -> Generator[None, MockConfigEntry, None]: + """Set up Kostal Plenticore integration for testing.""" + with patch( + "homeassistant.components.kostal_plenticore.Plenticore", autospec=True + ) as mock_api_class: + # setup + plenticore = mock_api_class.return_value + plenticore.async_setup = AsyncMock() + plenticore.async_setup.return_value = True + + plenticore.device_info = DeviceInfo( + configuration_url="http://192.168.1.2", + identifiers={("kostal_plenticore", "12345")}, + manufacturer="Kostal", + model="PLENTICORE plus 10", + name="scb", + sw_version="IOC: 01.45 MC: 01.46", + ) + + plenticore.client = MagicMock() + + plenticore.client.get_version = AsyncMock() + plenticore.client.get_version.return_value = VersionData( + { + "api_version": "0.2.0", + "hostname": "scb", + "name": "PUCK RESTful API", + "sw_version": "01.16.05025", + } + ) + + plenticore.client.get_me = AsyncMock() + plenticore.client.get_me.return_value = MeData( + { + "locked": False, + "active": True, + "authenticated": True, + "permissions": [], + "anonymous": False, + "role": "USER", + } + ) + + plenticore.client.get_process_data = AsyncMock() + plenticore.client.get_process_data.return_value = { + "devices:local": ["HomeGrid_P", "HomePv_P"] + } + + plenticore.client.get_settings = AsyncMock() + plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData( + { + "id": "Battery:MinSoc", + "unit": "%", + "default": "None", + "min": 5, + "max": 100, + "type": "byte", + "access": "readwrite", + } + ) + ] + } + + mock_config_entry = MockConfigEntry( + entry_id="2ab8dd92a62787ddfe213a67e09406bd", + title="scb", + domain="kostal_plenticore", + data={"host": "192.168.1.2", "password": "SecretPassword"}, + ) + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + yield mock_config_entry diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py new file mode 100644 index 00000000000..56af8bafe06 --- /dev/null +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -0,0 +1,49 @@ +"""Test Kostal Plenticore diagnostics.""" +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_entry_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry +): + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "config_entry": { + "entry_id": "2ab8dd92a62787ddfe213a67e09406bd", + "version": 1, + "domain": "kostal_plenticore", + "title": "scb", + "data": {"host": "192.168.1.2", "password": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": None, + "disabled_by": None, + }, + "client": { + "version": "Version(api_version=0.2.0, hostname=scb, name=PUCK RESTful API, sw_version=01.16.05025)", + "me": "Me(locked=False, active=True, authenticated=True, permissions=[] anonymous=False role=USER)", + "available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]}, + "available_settings_data": { + "devices:local": [ + "SettingsData(id=Battery:MinSoc, unit=%, default=None, min=5, max=100,type=byte, access=readwrite)" + ] + }, + }, + "device": { + "configuration_url": "http://192.168.1.2", + "identifiers": "**REDACTED**", + "manufacturer": "Kostal", + "model": "PLENTICORE plus 10", + "name": "scb", + "sw_version": "IOC: 01.45 MC: 01.46", + }, + } From fb82013c3992d5011986cae4e3a717108434e41a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Feb 2022 09:20:56 -1000 Subject: [PATCH 032/165] Fix powerwall data incompatibility with energy integration (#67245) --- homeassistant/components/powerwall/sensor.py | 87 ++++++++++++++------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index a48726211b2..bc8ab1c0215 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from tesla_powerwall import MeterType +from tesla_powerwall import Meter, MeterType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -28,7 +28,6 @@ from .models import PowerwallData, PowerwallRuntimeData _METER_DIRECTION_EXPORT = "export" _METER_DIRECTION_IMPORT = "import" -_METER_DIRECTIONS = [_METER_DIRECTION_EXPORT, _METER_DIRECTION_IMPORT] async def async_setup_entry( @@ -42,20 +41,20 @@ async def async_setup_entry( assert coordinator is not None data: PowerwallData = coordinator.data entities: list[ - PowerWallEnergySensor | PowerWallEnergyDirectionSensor | PowerWallChargeSensor - ] = [] - for meter in data.meters.meters: - entities.append(PowerWallEnergySensor(powerwall_data, meter)) - for meter_direction in _METER_DIRECTIONS: - entities.append( - PowerWallEnergyDirectionSensor( - powerwall_data, - meter, - meter_direction, - ) - ) + PowerWallEnergySensor + | PowerWallImportSensor + | PowerWallExportSensor + | PowerWallChargeSensor + ] = [PowerWallChargeSensor(powerwall_data)] - entities.append(PowerWallChargeSensor(powerwall_data)) + for meter in data.meters.meters: + entities.extend( + [ + PowerWallEnergySensor(powerwall_data, meter), + PowerWallExportSensor(powerwall_data, meter), + PowerWallImportSensor(powerwall_data, meter), + ] + ) async_add_entities(entities) @@ -128,18 +127,54 @@ class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Initialize the sensor.""" super().__init__(powerwall_data) self._meter = meter - self._meter_direction = meter_direction - self._attr_name = ( - f"Powerwall {self._meter.value.title()} {self._meter_direction.title()}" - ) - self._attr_unique_id = ( - f"{self.base_unique_id}_{self._meter.value}_{self._meter_direction}" - ) + self._attr_name = f"Powerwall {meter.value.title()} {meter_direction.title()}" + self._attr_unique_id = f"{self.base_unique_id}_{meter.value}_{meter_direction}" + + @property + def available(self) -> bool: + """Check if the reading is actually available. + + The device reports 0 when something goes wrong which + we do not want to include in statistics and its a + transient data error. + """ + return super().available and self.native_value != 0 + + @property + def meter(self) -> Meter: + """Get the meter for the sensor.""" + return self.data.meters.get_meter(self._meter) + + +class PowerWallExportSensor(PowerWallEnergyDirectionSensor): + """Representation of an Powerwall Export sensor.""" + + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + ) -> None: + """Initialize the sensor.""" + super().__init__(powerwall_data, meter, _METER_DIRECTION_EXPORT) @property def native_value(self) -> float: """Get the current value in kWh.""" - meter = self.data.meters.get_meter(self._meter) - if self._meter_direction == _METER_DIRECTION_EXPORT: - return meter.get_energy_exported() - return meter.get_energy_imported() + return abs(self.meter.get_energy_exported()) + + +class PowerWallImportSensor(PowerWallEnergyDirectionSensor): + """Representation of an Powerwall Import sensor.""" + + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + ) -> None: + """Initialize the sensor.""" + super().__init__(powerwall_data, meter, _METER_DIRECTION_IMPORT) + + @property + def native_value(self) -> float: + """Get the current value in kWh.""" + return abs(self.meter.get_energy_imported()) From 2d53e222ffb5c1fc717ada0f37eb273fbaab9483 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 11:52:14 -0800 Subject: [PATCH 033/165] Improve not shown handling (#67247) --- .../components/camera/media_source.py | 3 +++ .../components/media_source/__init__.py | 2 +- tests/components/camera/test_media_source.py | 1 + tests/components/media_source/test_init.py | 26 ++++++++++++++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index c61cbef146a..ab7661fefe2 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -81,11 +81,13 @@ class CameraMediaSource(MediaSource): # Root. List cameras. component: EntityComponent = self.hass.data[DOMAIN] children = [] + not_shown = 0 for camera in component.entities: camera = cast(Camera, camera) stream_type = camera.frontend_stream_type if stream_type not in supported_stream_types: + not_shown += 1 continue children.append( @@ -111,4 +113,5 @@ class CameraMediaSource(MediaSource): can_expand=True, children_media_class=MEDIA_CLASS_VIDEO, children=children, + not_shown=not_shown, ) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 2bcd80a39ab..3c42016f8f7 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -119,7 +119,7 @@ async def async_browse_media( item.children = [ child for child in item.children if child.can_expand or content_filter(child) ] - item.not_shown = old_count - len(item.children) + item.not_shown += old_count - len(item.children) return item diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 3a3558419e5..54d6ef6279e 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -35,6 +35,7 @@ async def test_browsing_filter_non_hls(hass, mock_camera_web_rtc): assert item is not None assert item.title == "Camera" assert len(item.children) == 0 + assert item.not_shown == 2 async def test_resolving(hass, mock_camera_hls): diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 2655000efc9..491b1972cb6 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -6,7 +6,7 @@ import yarl from homeassistant.components import media_source from homeassistant.components.media_player import MEDIA_CLASS_DIRECTORY, BrowseError -from homeassistant.components.media_source import const +from homeassistant.components.media_source import const, models from homeassistant.setup import async_setup_component @@ -60,6 +60,30 @@ async def test_async_browse_media(hass): media.children[0].title = "Epic Sax Guy 10 Hours" assert media.not_shown == 1 + # Test content filter adds to original not_shown + orig_browse = models.MediaSourceItem.async_browse + + async def not_shown_browse(self): + """Patch browsed item to set not_shown base value.""" + item = await orig_browse(self) + item.not_shown = 10 + return item + + with patch( + "homeassistant.components.media_source.models.MediaSourceItem.async_browse", + not_shown_browse, + ): + media = await media_source.async_browse_media( + hass, + "", + content_filter=lambda item: item.media_content_type.startswith("video/"), + ) + assert isinstance(media, media_source.models.BrowseMediaSource) + assert media.title == "media" + assert len(media.children) == 1, media.children + media.children[0].title = "Epic Sax Guy 10 Hours" + assert media.not_shown == 11 + # Test invalid media content with pytest.raises(BrowseError): await media_source.async_browse_media(hass, "invalid") From f39aea70e60afa6146849353bab1a860d78c0f13 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 11:35:39 -0800 Subject: [PATCH 034/165] =?UTF-8?q?Give=20Sonos=20media=20browse=20folders?= =?UTF-8?q?=20Sonos=20logos=20to=20distinguish=20from=20media=E2=80=A6=20(?= =?UTF-8?q?#67248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/sonos/media_browser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 2272ceb183f..b2d881e8bf2 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -267,6 +267,7 @@ async def root_payload( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="favorites", + thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", can_play=False, can_expand=True, ) @@ -281,6 +282,7 @@ async def root_payload( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="library", + thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", can_play=False, can_expand=True, ) From d16f0ba32b0598df4c2c72e3cffe4e2928b91d8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Feb 2022 09:37:19 -1000 Subject: [PATCH 035/165] Prevent the wrong WiZ device from being used when the IP is a different device (#67250) --- homeassistant/components/wiz/__init__.py | 9 +++++++++ tests/components/wiz/test_init.py | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 7bea86d323c..d739c571c8b 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -60,6 +60,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await bulb.async_close() raise ConfigEntryNotReady(f"{ip_address}: {err}") from err + if bulb.mac != entry.unique_id: + # The ip address of the bulb has changed and its likely offline + # and another WiZ device has taken the IP. Avoid setting up + # since its the wrong device. As soon as the device comes back + # online the ip will get updated and setup will proceed. + raise ConfigEntryNotReady( + "Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" + ) + async def _async_update() -> None: """Update the WiZ device.""" try: diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index fb21e930efd..58afb5c944a 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -50,3 +50,11 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: _, entry = await async_setup_integration(hass, wizlight=bulb) assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY bulb.async_close.assert_called_once() + + +async def test_wrong_device_now_has_our_ip(hass: HomeAssistant) -> None: + """Test setup is retried when the wrong device is found.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.mac = "dddddddddddd" + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY From 241611ff0578be7e0c70f6972057a2a1b00443b6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 12:19:56 -0800 Subject: [PATCH 036/165] Kodi/Roku: Add brand logos to brand folders at root level (#67251) --- homeassistant/components/kodi/browse_media.py | 3 +++ homeassistant/components/roku/browse_media.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index e0fdb0f73fd..519c4dc7c1b 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -224,6 +224,9 @@ async def library_payload(hass): ) ) + for child in library_info.children: + child.thumbnail = "https://brands.home-assistant.io/_/kodi/logo.png" + with contextlib.suppress(media_source.BrowseError): item = await media_source.async_browse_media(hass, None) # If domain is None, it's overview of available sources diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index d8cd540e613..72b572e8d3e 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -135,6 +135,9 @@ async def root_payload( ) ) + for child in children: + child.thumbnail = "https://brands.home-assistant.io/_/roku/logo.png" + try: browse_item = await media_source.async_browse_media(hass, None) From 86f511ac6a8072d4c1b6fce183b5aba56cc1c917 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 14:01:20 -0800 Subject: [PATCH 037/165] Bump hass-nabucasa to 0.54.0 (#67252) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e9548c03ba6..d5d0c2c0370 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.53.1"], + "requirements": ["hass-nabucasa==0.54.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1bd905e6abd..fd8fe9c0681 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 -hass-nabucasa==0.53.1 +hass-nabucasa==0.54.0 home-assistant-frontend==20220224.0 httpx==0.21.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 713cd7dd33e..a1ed674536c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ habitipy==0.2.0 hangups==0.4.17 # homeassistant.components.cloud -hass-nabucasa==0.53.1 +hass-nabucasa==0.54.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f94c277f31..5f9291dc2e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -532,7 +532,7 @@ habitipy==0.2.0 hangups==0.4.17 # homeassistant.components.cloud -hass-nabucasa==0.53.1 +hass-nabucasa==0.54.0 # homeassistant.components.tasmota hatasmota==0.3.1 From f21ee7a748f0fe9ed4976d34d4b38849bab5d3ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 01:02:13 -0800 Subject: [PATCH 038/165] Fix camera content type while browsing (#67256) --- .../components/camera/media_source.py | 15 +++++++++------ tests/components/camera/test_media_source.py | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index ab7661fefe2..e65aabe459d 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -73,10 +73,7 @@ class CameraMediaSource(MediaSource): if item.identifier: raise BrowseError("Unknown item") - supported_stream_types: list[str | None] = [None] - - if "stream" in self.hass.config.components: - supported_stream_types.append(STREAM_TYPE_HLS) + can_stream_hls = "stream" in self.hass.config.components # Root. List cameras. component: EntityComponent = self.hass.data[DOMAIN] @@ -86,7 +83,13 @@ class CameraMediaSource(MediaSource): camera = cast(Camera, camera) stream_type = camera.frontend_stream_type - if stream_type not in supported_stream_types: + if stream_type is None: + content_type = camera.content_type + + elif can_stream_hls and stream_type == STREAM_TYPE_HLS: + content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] + + else: not_shown += 1 continue @@ -95,7 +98,7 @@ class CameraMediaSource(MediaSource): domain=DOMAIN, identifier=camera.entity_id, media_class=MEDIA_CLASS_VIDEO, - media_content_type=FORMAT_CONTENT_TYPE[HLS_PROVIDER], + media_content_type=content_type, title=camera.name, thumbnail=f"/api/camera_proxy/{camera.entity_id}", can_play=True, diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 54d6ef6279e..b9fb22c9ed8 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -15,21 +15,35 @@ async def setup_media_source(hass): assert await async_setup_component(hass, "media_source", {}) -async def test_browsing(hass, mock_camera_hls): +async def test_browsing_hls(hass, mock_camera_hls): """Test browsing camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None assert item.title == "Camera" assert len(item.children) == 0 + assert item.not_shown == 2 # Adding stream enables HLS camera hass.config.components.add("stream") item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 0 assert len(item.children) == 2 + assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] -async def test_browsing_filter_non_hls(hass, mock_camera_web_rtc): +async def test_browsing_mjpeg(hass, mock_camera): + """Test browsing camera media source.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item is not None + assert item.title == "Camera" + assert len(item.children) == 2 + assert item.not_shown == 0 + assert item.children[0].media_content_type == "image/jpg" + assert item.children[1].media_content_type == "image/png" + + +async def test_browsing_filter_web_rtc(hass, mock_camera_web_rtc): """Test browsing camera media source hides non-HLS cameras.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None From 5b5aa3d604c4eb75eeb7f3db8ca72425f8864122 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 00:58:45 -0800 Subject: [PATCH 039/165] Kodi: Mark MJPEG cameras using PNGs as incompatible (#67257) --- homeassistant/components/kodi/browse_media.py | 13 ++++++++++++- homeassistant/components/kodi/media_player.py | 11 +++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 519c4dc7c1b..73247d23a9d 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -186,6 +186,15 @@ async def item_payload(item, get_thumbnail_url=None): ) +def media_source_content_filter(item: BrowseMedia) -> bool: + """Content filter for media sources.""" + # Filter out cameras using PNG over MJPEG. They don't work in Kodi. + return not ( + item.media_content_id.startswith("media-source://camera/") + and item.media_content_type == "image/png" + ) + + async def library_payload(hass): """ Create response payload to describe contents of a specific library. @@ -228,7 +237,9 @@ async def library_payload(hass): child.thumbnail = "https://brands.home-assistant.io/_/kodi/logo.png" with contextlib.suppress(media_source.BrowseError): - item = await media_source.async_browse_media(hass, None) + item = await media_source.async_browse_media( + hass, None, content_filter=media_source_content_filter + ) # If domain is None, it's overview of available sources if item.domain is None: library_info.children.extend(item.children) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 56b0abb6a15..53798a7ccd9 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -77,7 +77,12 @@ from homeassistant.helpers.network import is_internal_request from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .browse_media import build_item_response, get_media_info, library_payload +from .browse_media import ( + build_item_response, + get_media_info, + library_payload, + media_source_content_filter, +) from .const import ( CONF_WS_PORT, DATA_CONNECTION, @@ -916,7 +921,9 @@ class KodiEntity(MediaPlayerEntity): return await library_payload(self.hass) if media_source.is_media_source_id(media_content_id): - return await media_source.async_browse_media(self.hass, media_content_id) + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=media_source_content_filter + ) payload = { "search_type": media_content_type, From 5cffec8b23daa4f5245a6025a6f81c957cda08c0 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 26 Feb 2022 00:56:07 -0800 Subject: [PATCH 040/165] Fix Doorbird warning if registering favorites fail (#67262) --- homeassistant/components/doorbird/__init__.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 06264153af2..502ff453a27 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus import logging +from typing import Any from aiohttp import web from doorbirdpy import DoorBird @@ -166,7 +167,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _async_register_events(hass, doorstation): +async def _async_register_events( + hass: HomeAssistant, doorstation: ConfiguredDoorBird +) -> bool: try: await hass.async_add_executor_job(doorstation.register_events, hass) except requests.exceptions.HTTPError: @@ -243,7 +246,7 @@ class ConfiguredDoorBird: """Get token for device.""" return self._token - def register_events(self, hass): + def register_events(self, hass: HomeAssistant) -> None: """Register events on device.""" # Get the URL of this server hass_url = get_url(hass) @@ -258,9 +261,10 @@ class ConfiguredDoorBird: favorites = self.device.favorites() for event in self.doorstation_events: - self._register_event(hass_url, event, favs=favorites) - - _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) + if self._register_event(hass_url, event, favs=favorites): + _LOGGER.info( + "Successfully registered URL for %s on %s", event, self.name + ) @property def slug(self): @@ -270,21 +274,25 @@ class ConfiguredDoorBird: def _get_event_name(self, event): return f"{self.slug}_{event}" - def _register_event(self, hass_url, event, favs=None): + def _register_event( + self, hass_url: str, event: str, favs: dict[str, Any] | None = None + ) -> bool: """Add a schedule entry in the device for a sensor.""" url = f"{hass_url}{API_URL}/{event}?token={self._token}" # Register HA URL as webhook if not already, then get the ID if self.webhook_is_registered(url, favs=favs): - return + return True self.device.change_favorite("http", f"Home Assistant ({event})", url) if not self.webhook_is_registered(url): _LOGGER.warning( - 'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"', + 'Unable to set favorite URL "%s". ' 'Event "%s" will not fire', url, event, ) + return False + return True def webhook_is_registered(self, url, favs=None) -> bool: """Return whether the given URL is registered as a device favorite.""" From a3cdc2facb504ed69a160159a6c0d3cd7d467654 Mon Sep 17 00:00:00 2001 From: pailloM <56462552+pailloM@users.noreply.github.com> Date: Sat, 26 Feb 2022 03:46:16 -0500 Subject: [PATCH 041/165] Re-enable apcupsd (#67264) --- homeassistant/components/apcupsd/manifest.json | 1 - requirements_all.txt | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 18d5549ef9a..13a08685c68 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,5 +1,4 @@ { - "disabled": "Integration library not compatible with Python 3.10", "domain": "apcupsd", "name": "apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", diff --git a/requirements_all.txt b/requirements_all.txt index a1ed674536c..9984b08e920 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -322,6 +322,9 @@ anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav anthemav==1.2.0 +# homeassistant.components.apcupsd +apcaccess==0.0.13 + # homeassistant.components.apprise apprise==0.9.7 From 61b43860537995f13bae9783bf26ad261d9679f8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 26 Feb 2022 19:37:24 +0100 Subject: [PATCH 042/165] Fix dhcp None hostname (#67289) * Fix dhcp None hostname * Test handle None hostname --- homeassistant/components/dhcp/__init__.py | 16 +++++++++------- tests/components/dhcp/test_init.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index a3de0e51708..0b5f8a49a34 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -159,7 +159,7 @@ class WatcherBase: async def async_start(self): """Start the watcher.""" - def process_client(self, ip_address, hostname, mac_address): + def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None: """Process a client.""" return run_callback_threadsafe( self.hass.loop, @@ -170,7 +170,9 @@ class WatcherBase: ).result() @callback - def async_process_client(self, ip_address, hostname, mac_address): + def async_process_client( + self, ip_address: str, hostname: str, mac_address: str + ) -> None: """Process a client.""" made_ip_address = make_ip_address(ip_address) @@ -355,15 +357,15 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): async def async_start(self): """Stop watching for device tracker registrations.""" self._unsub = async_dispatcher_connect( - self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_state + self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data ) @callback - def _async_process_device_state(self, data: dict[str, Any]) -> None: + def _async_process_device_data(self, data: dict[str, str | None]) -> None: """Process a device tracker state.""" - ip_address = data.get(ATTR_IP) - hostname = data.get(ATTR_HOST_NAME, "") - mac_address = data.get(ATTR_MAC) + ip_address = data[ATTR_IP] + hostname = data[ATTR_HOST_NAME] or "" + mac_address = data[ATTR_MAC] if ip_address is None or mac_address is None: return diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index d1b8d72be67..a809d6eb5ab 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -663,6 +663,28 @@ async def test_device_tracker_registered(hass): await hass.async_block_till_done() +async def test_device_tracker_registered_hostname_none(hass): + """Test handle None hostname.""" + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + async_dispatcher_send( + hass, + CONNECTED_DEVICE_REGISTERED, + {"ip": "192.168.210.56", "mac": "b8b7f16db533", "host_name": None}, + ) + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + async def test_device_tracker_hostname_and_macaddress_after_start(hass): """Test matching based on hostname and macaddress after start.""" From 23846eb1209be49bf4085396f76093279a6ab0a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 14:12:33 -0800 Subject: [PATCH 043/165] Bump frontend to 20220226.0 (#67313) --- 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 f9ad2bd428d..4b28744d0e3 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==20220224.0" + "home-assistant-frontend==20220226.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fd8fe9c0681..a36f21efd6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220224.0 +home-assistant-frontend==20220226.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 9984b08e920..626406a0bff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -843,7 +843,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220224.0 +home-assistant-frontend==20220226.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f9291dc2e1..964a793b9c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220224.0 +home-assistant-frontend==20220226.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 8c3c8ff1d4918fc707b6568d3a6f3d76e16dec93 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 14:13:19 -0800 Subject: [PATCH 044/165] Bumped version to 2022.3.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 95f2b3d9f57..b3f86ff9d6a 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 = 3 -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 00a0d4a695f..7cacc226032 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b3 +version = 2022.3.0b4 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From b468cc8c9e90d6d1f482f16d2a203ceaa23d478d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 27 Feb 2022 00:23:56 +0100 Subject: [PATCH 045/165] Remove redundant type cast (#67317) --- homeassistant/components/frontend/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c6812f4d9de..803b093fd40 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -7,7 +7,7 @@ import json import logging import os import pathlib -from typing import Any, TypedDict, cast +from typing import Any, TypedDict from aiohttp import hdrs, web, web_urldispatcher import jinja2 @@ -313,7 +313,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: # pylint: disable=import-outside-toplevel import hass_frontend - return cast(pathlib.Path, hass_frontend.where()) + return hass_frontend.where() async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: From 2639965b2419c4862eec111022c340f2752e107b Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 27 Feb 2022 11:19:20 -0800 Subject: [PATCH 046/165] Bump pyoverkiz to 1.3.9 in Overkiz integration (#67339) --- 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 1c5d6b1b685..5e8fe27e21e 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.8" + "pyoverkiz==1.3.9" ], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 626406a0bff..f04552f55d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.8 +pyoverkiz==1.3.9 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 964a793b9c1..49995b5de6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1122,7 +1122,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.8 +pyoverkiz==1.3.9 # homeassistant.components.openweathermap pyowm==3.2.0 From 6d5be0167733903f0e461942dec9a30c15611553 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Feb 2022 12:04:22 -0800 Subject: [PATCH 047/165] Guard for index error in picnic (#67345) --- homeassistant/components/picnic/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 773142a0109..9f387858e5f 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -112,7 +112,7 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): next_delivery = ( copy.deepcopy(next_deliveries[-1]) if next_deliveries else {} ) - last_order = copy.deepcopy(deliveries[0]) + last_order = copy.deepcopy(deliveries[0]) if deliveries else {} except (KeyError, TypeError): # A KeyError or TypeError indicate that the response contains unexpected data return {}, {} From aee2a8bc511427c1e0859a25d02d7de2f9ece115 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Feb 2022 12:59:05 -0800 Subject: [PATCH 048/165] Guard for non-string inputs in Alexa (#67348) --- homeassistant/components/alexa/capabilities.py | 2 ++ tests/components/alexa/test_capabilities.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 133ad4f2bda..327d5973892 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -822,6 +822,8 @@ class AlexaInputController(AlexaCapability): """Return list of supported inputs.""" input_list = [] for source in source_list: + if not isinstance(source, str): + continue formatted_source = ( source.lower().replace("-", "").replace("_", "").replace(" ", "") ) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 8a9a40e3217..d24849e1006 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -182,7 +182,7 @@ async def test_api_increase_color_temp(hass, result, initial): @pytest.mark.parametrize( "domain,payload,source_list,idx", [ - ("media_player", "GAME CONSOLE", ["tv", "game console"], 1), + ("media_player", "GAME CONSOLE", ["tv", "game console", 10000], 1), ("media_player", "SATELLITE TV", ["satellite-tv", "game console"], 0), ("media_player", "SATELLITE TV", ["satellite_tv", "game console"], 0), ("media_player", "BAD DEVICE", ["satellite_tv", "game console"], None), From e4c8ac64a41511ea90d9a48fe9590f27107b92b2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Feb 2022 00:50:42 -0600 Subject: [PATCH 049/165] Bump plexapi to 4.10.0 (#67364) --- 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 238c25ad917..85a060ae7cd 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.2", + "plexapi==4.10.0", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index f04552f55d1..e8ace669520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1262,7 +1262,7 @@ pillow==9.0.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.9.2 +plexapi==4.10.0 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49995b5de6a..770acbd7d61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ pilight==0.1.1 pillow==9.0.1 # homeassistant.components.plex -plexapi==4.9.2 +plexapi==4.10.0 # homeassistant.components.plex plexauth==0.0.6 From 06791d42f2c0f0714a0013ed0f916390abaa47a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Feb 2022 13:19:50 +0000 Subject: [PATCH 050/165] Fix race when unsubscribing from MQTT topics (#67376) * Fix race when unsubscribing from MQTT topics * Improve test --- homeassistant/components/mqtt/__init__.py | 8 +++--- tests/components/mqtt/test_init.py | 32 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1982d1f3df5..107bc4660c2 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -967,10 +967,6 @@ class MQTT: self.subscriptions.remove(subscription) self._matching_subscriptions.cache_clear() - if any(other.topic == topic for other in self.subscriptions): - # Other subscriptions on topic remaining - don't unsubscribe. - return - # Only unsubscribe if currently connected. if self.connected: self.hass.async_create_task(self._async_unsubscribe(topic)) @@ -982,6 +978,10 @@ class MQTT: This method is a coroutine. """ + if any(other.topic == topic for other in self.subscriptions): + # Other subscriptions on topic remaining - don't unsubscribe. + return + async with self._paho_lock: result: int | None = None result, mid = await self.hass.async_add_executor_job( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index e589e447a01..7296d4e8101 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1056,6 +1056,38 @@ async def test_not_calling_unsubscribe_with_active_subscribers( assert not mqtt_client_mock.unsubscribe.called +async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): + """Test not calling unsubscribe() when other subscribers are active.""" + # Fake that the client is connected + mqtt_mock().connected = True + + calls_a = MagicMock() + calls_b = MagicMock() + + mqtt_client_mock.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", calls_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", calls_b) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test/state", "online") + await hass.async_block_till_done() + assert not calls_a.called + assert calls_b.called + + # We allow either calls [subscribe, unsubscribe, subscribe] or [subscribe, subscribe] + expected_calls_1 = [ + call.subscribe("test/state", 0), + call.unsubscribe("test/state"), + call.subscribe("test/state", 0), + ] + expected_calls_2 = [ + call.subscribe("test/state", 0), + call.subscribe("test/state", 0), + ] + assert mqtt_client_mock.mock_calls in (expected_calls_1, expected_calls_2) + + @pytest.mark.parametrize( "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], From 4423ecbe1c6ef0ef121c73aab2c4cc6bdc666406 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Feb 2022 18:38:08 -0600 Subject: [PATCH 051/165] Reduce magic in Sonos error handling fixture (#67401) --- homeassistant/components/sonos/helpers.py | 32 ++++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index fbc1d2642ea..a11847d2b0c 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException @@ -55,15 +55,9 @@ def soco_error( ) return None - # In order of preference: - # * SonosSpeaker instance - # * SoCo instance passed as an arg - # * SoCo instance (as self) - speaker_or_soco = getattr(self, "speaker", args_soco or self) - zone_name = speaker_or_soco.zone_name - # Prefer the entity_id if available, zone name as a fallback - # Needed as SonosSpeaker instances are not entities - target = getattr(self, "entity_id", zone_name) + if (target := _find_target_identifier(self, args_soco)) is None: + raise RuntimeError("Unexpected use of soco_error") from err + message = f"Error calling {function} on {target}: {err}" raise SonosUpdateError(message) from err @@ -80,6 +74,24 @@ def soco_error( return decorator +def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None: + """Extract the the best available target identifier from the provided instance object.""" + if entity_id := getattr(instance, "entity_id", None): + # SonosEntity instance + return entity_id + if zone_name := getattr(instance, "zone_name", None): + # SonosSpeaker instance + return zone_name + if speaker := getattr(instance, "speaker", None): + # Holds a SonosSpeaker instance attribute + return speaker.zone_name + if soco := getattr(instance, "soco", fallback_soco): + # Holds a SoCo instance attribute + # Only use attributes with no I/O + return soco._player_name or soco.ip_address # pylint: disable=protected-access + return None + + def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" if hostname.startswith("Sonos-"): From cd5056fdab72c0a2f3e834777b00ce8d14546dd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Feb 2022 14:39:13 -1000 Subject: [PATCH 052/165] Bump zeroconf to 0.38.4 (#67406) --- 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 fa3b8688c47..dc4c7c001ae 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.3"], + "requirements": ["zeroconf==0.38.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a36f21efd6b..c944b6c141c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,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.3 +zeroconf==0.38.4 # 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 e8ace669520..99c1a711d14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2536,7 +2536,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.3 +zeroconf==0.38.4 # homeassistant.components.zha zha-quirks==0.0.67 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 770acbd7d61..1201b07682a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1567,7 +1567,7 @@ yeelight==0.7.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.3 +zeroconf==0.38.4 # homeassistant.components.zha zha-quirks==0.0.67 From ee3be011a543b23ecf2b077ec51c4573b2f13ba6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Feb 2022 17:02:34 -0800 Subject: [PATCH 053/165] Bumped version to 2022.3.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 b3f86ff9d6a..e46509d2b84 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 = 3 -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 7cacc226032..58a3918d0ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b4 +version = 2022.3.0b5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From d766b1732352fbf5a10be2bb56fe7d710d87592a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Mar 2022 14:00:48 -1000 Subject: [PATCH 054/165] Partially revert powerwall abs change from #67245 (#67300) --- homeassistant/components/powerwall/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index bc8ab1c0215..93b3c64d18c 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -114,7 +114,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" - _attr_state_class = SensorStateClass.TOTAL_INCREASING + _attr_state_class = SensorStateClass.TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = SensorDeviceClass.ENERGY @@ -160,7 +160,7 @@ class PowerWallExportSensor(PowerWallEnergyDirectionSensor): @property def native_value(self) -> float: """Get the current value in kWh.""" - return abs(self.meter.get_energy_exported()) + return self.meter.get_energy_exported() class PowerWallImportSensor(PowerWallEnergyDirectionSensor): @@ -177,4 +177,4 @@ class PowerWallImportSensor(PowerWallEnergyDirectionSensor): @property def native_value(self) -> float: """Get the current value in kWh.""" - return abs(self.meter.get_energy_imported()) + return self.meter.get_energy_imported() From 26203e99246bb75f53aff6261073e32f55a39632 Mon Sep 17 00:00:00 2001 From: Jeff <34590663+jumbledbytes@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:37:11 -0800 Subject: [PATCH 055/165] Support disconnected Powerwall configuration (#67325) Co-authored-by: J. Nick Koston --- homeassistant/components/powerwall/binary_sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 868d9e3076d..fed47823c7f 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -110,6 +110,15 @@ class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): _attr_name = "Powerwall Charging" _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + @property + def available(self) -> bool: + """Powerwall is available.""" + # Return False if no battery is installed + return ( + super().available + and self.data.meters.get_meter(MeterType.BATTERY) is not None + ) + @property def unique_id(self) -> str: """Device Uniqueid.""" From aeac31c926636507f0d41ccfd398762015d1555c Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 2 Mar 2022 01:21:47 +0100 Subject: [PATCH 056/165] Add flipr API error detection and catch it correctly. (#67405) --- homeassistant/components/flipr/__init__.py | 14 +++++++-- homeassistant/components/flipr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flipr/test_sensor.py | 30 ++++++++++++++++++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 8379845982a..1a9f3dc0314 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from flipr_api import FliprAPIRestClient +from flipr_api.exceptions import FliprError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform @@ -11,6 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN, MANUFACTURER, NAME @@ -68,9 +70,15 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Fetch data from API endpoint.""" - return await self.hass.async_add_executor_job( - self.client.get_pool_measure_latest, self.flipr_id - ) + try: + data = await self.hass.async_add_executor_job( + self.client.get_pool_measure_latest, self.flipr_id + ) + except (FliprError) as error: + _LOGGER.error(error) + raise UpdateFailed from error + + return data class FliprEntity(CoordinatorEntity): diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 357b5aeb160..77388393d3f 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flipr", "requirements": [ - "flipr-api==1.4.1"], + "flipr-api==1.4.2"], "codeowners": [ "@cnico" ], diff --git a/requirements_all.txt b/requirements_all.txt index 99c1a711d14..ef6c59a53ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ fixerio==1.0.0a0 fjaraskupan==1.0.2 # homeassistant.components.flipr -flipr-api==1.4.1 +flipr-api==1.4.2 # homeassistant.components.flux_led flux_led==0.28.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1201b07682a..85b53a3408c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -431,7 +431,7 @@ fivem-api==0.1.2 fjaraskupan==1.0.2 # homeassistant.components.flipr -flipr-api==1.4.1 +flipr-api==1.4.2 # homeassistant.components.flux_led flux_led==0.28.27 diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 7fd04fbc992..45816801472 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import patch +from flipr_api.exceptions import FliprError + from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import ( ATTR_ICON, @@ -84,3 +86,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" assert state.state == "0.23654886" + + +async def test_error_flipr_api_sensors(hass: HomeAssistant) -> None: + """Test the Flipr sensors error.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test_entry_unique_id", + data={ + CONF_EMAIL: "toto@toto.com", + CONF_PASSWORD: "myPassword", + CONF_FLIPR_ID: "myfliprid", + }, + ) + + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + with patch( + "flipr_api.FliprAPIRestClient.get_pool_measure_latest", + side_effect=FliprError("Error during flipr data retrieval..."), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Check entity is not generated because of the FliprError raised. + entity = registry.async_get("sensor.flipr_myfliprid_red_ox") + assert entity is None From f1620cbb2e00849c21f6fa39a26e89576a0fba83 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 16:56:05 -0800 Subject: [PATCH 057/165] Add support for detecting hostname based addresses as internal (#67407) --- homeassistant/helpers/network.py | 4 +- tests/components/roku/test_media_player.py | 13 ++++-- tests/helpers/test_network.py | 51 ++++++++++++++++------ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 9d7780ab900..a8c4b3cf458 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -25,7 +25,9 @@ class NoURLAvailableError(HomeAssistantError): def is_internal_request(hass: HomeAssistant) -> bool: """Test if the current request is internal.""" try: - _get_internal_url(hass, require_current_request=True) + get_url( + hass, allow_external=False, allow_cloud=False, require_current_request=True + ) return True except NoURLAvailableError: return False diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 79b996530e3..050814e3817 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -759,7 +759,10 @@ async def test_media_browse( assert msg["result"]["children"][0]["title"] == "Roku Channel Store" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "11" - assert "/browse_media/app/11" in msg["result"]["children"][0]["thumbnail"] + assert ( + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/11" + ) assert msg["result"]["children"][0]["can_play"] # test invalid media type @@ -1016,14 +1019,18 @@ async def test_tv_media_browse( assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" assert ( - "/browse_media/app/tvinput.hdmi2" in msg["result"]["children"][0]["thumbnail"] + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/tvinput.hdmi2" ) assert msg["result"]["children"][0]["can_play"] assert msg["result"]["children"][3]["title"] == "Roku Channel Store" assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][3]["media_content_id"] == "11" - assert "/browse_media/app/11" in msg["result"]["children"][3]["thumbnail"] + assert ( + msg["result"]["children"][3]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/11" + ) assert msg["result"]["children"][3]["can_play"] # test channels diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 15a9b8d1ff8..0838375fd1f 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -20,6 +20,17 @@ from homeassistant.helpers.network import ( from tests.common import mock_component +@pytest.fixture(name="mock_current_request") +def mock_current_request_mock(): + """Mock the current request.""" + mock_current_request = Mock(name="mock_request") + with patch( + "homeassistant.helpers.network.http.current_request", + Mock(get=mock_current_request), + ): + yield mock_current_request + + async def test_get_url_internal(hass: HomeAssistant): """Test getting an instance URL when the user has set an internal URL.""" assert hass.config.internal_url is None @@ -611,7 +622,7 @@ async def test_get_current_request_url_with_known_host( get_url(hass, require_current_request=True) -async def test_is_internal_request(hass: HomeAssistant): +async def test_is_internal_request(hass: HomeAssistant, mock_current_request): """Test if accessing an instance on its internal URL.""" # Test with internal URL: http://example.local:8123 await async_process_ha_core_config( @@ -620,18 +631,16 @@ async def test_is_internal_request(hass: HomeAssistant): ) assert hass.config.internal_url == "http://example.local:8123" + + # No request active + mock_current_request.return_value = None assert not is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="example.local" - ): - assert is_internal_request(hass) + mock_current_request.return_value = Mock(url="http://example.local:8123") + assert is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.example.local", - ): - assert not is_internal_request(hass) + mock_current_request.return_value = Mock(url="http://no_match.example.local:8123") + assert not is_internal_request(hass) # Test with internal URL: http://192.168.0.1:8123 await async_process_ha_core_config( @@ -642,10 +651,26 @@ async def test_is_internal_request(hass: HomeAssistant): assert hass.config.internal_url == "http://192.168.0.1:8123" assert not is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1" + mock_current_request.return_value = Mock(url="http://192.168.0.1:8123") + assert is_internal_request(hass) + + # Test for matching against local IP + hass.config.api = Mock(use_ssl=False, local_ip="192.168.123.123", port=8123) + for allowed in ("127.0.0.1", "192.168.123.123"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url + + # Test for matching against HassOS hostname + with patch.object( + hass.components.hassio, "is_hassio", return_value=True + ), patch.object( + hass.components.hassio, + "get_host_info", + return_value={"hostname": "hellohost"}, ): - assert is_internal_request(hass) + for allowed in ("hellohost", "hellohost.local"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url async def test_is_hass_url(hass): From 768a0311287f8af1a47b5b42f2233ed6280dbbd8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 15:14:14 -0800 Subject: [PATCH 058/165] Restore children media class (#67409) --- homeassistant/components/dlna_dms/dms.py | 6 ++++-- .../components/media_player/browse_media.py | 16 +++++++--------- tests/components/cast/test_media_player.py | 5 +++++ tests/components/dlna_dmr/test_media_player.py | 2 ++ tests/components/motioneye/test_media_source.py | 9 +++++++++ 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index d3a65448f84..1166d1db6db 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -575,7 +575,8 @@ class DmsDeviceSource: children=children, ) - media_source.calculate_children_class() + if media_source.children: + media_source.calculate_children_class() return media_source @@ -645,7 +646,8 @@ class DmsDeviceSource: thumbnail=self._didl_thumbnail_url(item), ) - media_source.calculate_children_class() + if media_source.children: + media_source.calculate_children_class() return media_source diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 26494e4c8a7..fa825042817 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from urllib.parse import quote import yarl @@ -74,11 +75,15 @@ class BrowseMedia: def as_dict(self, *, parent: bool = True) -> dict: """Convert Media class to browse media dictionary.""" - response = { + if self.children_media_class is None and self.children: + self.calculate_children_class() + + response: dict[str, Any] = { "title": self.title, "media_class": self.media_class, "media_content_type": self.media_content_type, "media_content_id": self.media_content_id, + "children_media_class": self.children_media_class, "can_play": self.can_play, "can_expand": self.can_expand, "thumbnail": self.thumbnail, @@ -87,11 +92,7 @@ class BrowseMedia: if not parent: return response - if self.children_media_class is None: - self.calculate_children_class() - response["not_shown"] = self.not_shown - response["children_media_class"] = self.children_media_class if self.children: response["children"] = [ @@ -104,11 +105,8 @@ class BrowseMedia: def calculate_children_class(self) -> None: """Count the children media classes and calculate the correct class.""" - if self.children is None or len(self.children) == 0: - return - self.children_media_class = MEDIA_CLASS_DIRECTORY - + assert self.children is not None proposed_class = self.children[0].media_class if all(child.media_class == proposed_class for child in self.children): self.children_media_class = proposed_class diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 663941de77a..40a1269557d 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -857,6 +857,7 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_1 in response["result"]["children"] @@ -868,6 +869,7 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_2 in response["result"]["children"] @@ -911,6 +913,7 @@ async def test_entity_browse_media_audio_only( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_1 not in response["result"]["children"] @@ -922,6 +925,7 @@ async def test_entity_browse_media_audio_only( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_2 in response["result"]["children"] @@ -1858,6 +1862,7 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client): "can_play": False, "can_expand": True, "thumbnail": "https://brands.home-assistant.io/_/spotify/logo.png", + "children_media_class": None, } assert expected_child in response["result"]["children"] diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 896968557c1..a9ac5946f30 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -961,6 +961,7 @@ async def test_browse_media( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_video in response["result"]["children"] @@ -972,6 +973,7 @@ async def test_browse_media( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_audio in response["result"]["children"] diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 6c0e46b34c6..6979d5c645d 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -111,6 +111,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "directory", } ], "not_shown": 0, @@ -143,6 +144,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "directory", } ], "not_shown": 0, @@ -174,6 +176,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "video", }, { "title": "Images", @@ -186,6 +189,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "image", }, ], "not_shown": 0, @@ -220,6 +224,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "directory", } ], "not_shown": 0, @@ -255,6 +260,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": True, "can_expand": False, "thumbnail": "http://movie", + "children_media_class": None, }, { "title": "00-36-49.mp4", @@ -268,6 +274,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": True, "can_expand": False, "thumbnail": "http://movie", + "children_media_class": None, }, { "title": "00-02-27.mp4", @@ -281,6 +288,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": True, "can_expand": False, "thumbnail": "http://movie", + "children_media_class": None, }, ], "not_shown": 0, @@ -331,6 +339,7 @@ async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": False, "thumbnail": "http://image", + "children_media_class": None, } ], "not_shown": 0, From b31e570ec73fa2dcdf641e7169cde285d698b5bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Mar 2022 09:18:09 -1000 Subject: [PATCH 059/165] Avoid creating wiring select for Magic Home if its not supported (#67417) --- homeassistant/components/flux_led/select.py | 2 +- tests/components/flux_led/test_select.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index 4067110e336..63929740020 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -64,7 +64,7 @@ async def async_setup_entry( coordinator, base_unique_id, f"{name} Operating Mode", "operating_mode" ) ) - if device.wirings: + if device.wirings and device.wiring is not None: entities.append( FluxWiringsSelect(coordinator, base_unique_id, f"{name} Wiring", "wiring") ) diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py index b2a88b00fe0..91be62e5ab7 100644 --- a/tests/components/flux_led/test_select.py +++ b/tests/components/flux_led/test_select.py @@ -299,3 +299,23 @@ async def test_select_white_channel_type(hass: HomeAssistant) -> None: == WhiteChannelType.NATURAL.name.lower() ) assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_select_device_no_wiring(hass: HomeAssistant) -> None: + """Test select is not created if the device does not support wiring.""" + 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) + bulb = _mocked_bulb() + bulb.wiring = None + bulb.wirings = ["RGB", "GRB"] + bulb.raw_state = bulb.raw_state._replace(model_num=0x25) + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + wiring_entity_id = "select.bulb_rgbcw_ddeeff_wiring" + assert hass.states.get(wiring_entity_id) is None From 40d72b31884f91a7e70e7071f2529f07fd573647 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 1 Mar 2022 16:40:00 +0100 Subject: [PATCH 060/165] CONF_SLAVE do not have default 0 in a validator (#67418) --- homeassistant/components/modbus/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 4f910043d12..347e8d3fc72 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -209,7 +209,7 @@ def duplicate_entity_validator(config: dict) -> dict: addr += "_" + str(entry[CONF_COMMAND_ON]) if CONF_COMMAND_OFF in entry: addr += "_" + str(entry[CONF_COMMAND_OFF]) - addr += "_" + str(entry[CONF_SLAVE]) + addr += "_" + str(entry.get(CONF_SLAVE, 0)) if addr in addresses: err = f"Modbus {component}/{name} address {addr} is duplicate, second entry not loaded!" _LOGGER.warning(err) From 47812c6b911964abc211a2b888f761a4fdbca08d Mon Sep 17 00:00:00 2001 From: JeroenTuinstra <47460053+JeroenTuinstra@users.noreply.github.com> Date: Wed, 2 Mar 2022 00:12:54 +0100 Subject: [PATCH 061/165] Correct selector for remote integration line 50 (#67432) --- homeassistant/components/remote/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3130484d10b..bdeef15971e 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -47,7 +47,7 @@ send_command: required: true example: "Play" selector: - text: + object: num_repeats: name: Repeats description: The number of times you want to repeat the command(s). From 9a306e2a89cdac0cb6ba40b986b742e29d44c509 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 1 Mar 2022 18:19:34 +0100 Subject: [PATCH 062/165] Bump python-songpal to 0.14.1 (#67435) Changelog https://github.com/rytilahti/python-songpal/releases/tag/0.14.1 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 80a26a56b22..8825de877e5 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.14"], + "requirements": ["python-songpal==0.14.1"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ef6c59a53ef..672cdec1707 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1982,7 +1982,7 @@ python-smarttub==0.0.29 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.14 +python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85b53a3408c..3c76d58854c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1234,7 +1234,7 @@ python-picnic-api==1.1.0 python-smarttub==0.0.29 # homeassistant.components.songpal -python-songpal==0.14 +python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 From 99322e2658335629219e20c7d300d57f14948608 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Mar 2022 01:06:36 +0100 Subject: [PATCH 063/165] Fix CO2Signal having unknown data (#67453) --- homeassistant/components/co2signal/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 8d691b0e5c9..b10cd054ff9 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -92,14 +92,15 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE def available(self) -> bool: """Return True if entity is available.""" return ( - super().available - and self.coordinator.data["data"].get(self._description.key) is not None + super().available and self._description.key in self.coordinator.data["data"] ) @property def native_value(self) -> StateType: """Return sensor state.""" - return round(self.coordinator.data["data"][self._description.key], 2) # type: ignore[misc] + if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[misc] + return None + return round(value, 2) @property def native_unit_of_measurement(self) -> str | None: From fa01715bbbe59c3060a96e49e4a116da4918a4cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 16:05:23 -0800 Subject: [PATCH 064/165] Bump frontend to 20220301.0 (#67457) --- 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 4b28744d0e3..cc118e23dc9 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==20220226.0" + "home-assistant-frontend==20220301.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c944b6c141c..bd9e1da6a69 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220226.0 +home-assistant-frontend==20220301.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 672cdec1707..4d6dcefb10f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -843,7 +843,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220226.0 +home-assistant-frontend==20220301.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c76d58854c..c7828110fc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220226.0 +home-assistant-frontend==20220301.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 17bc8c64f8e65efb46d8ba98951ccfecbe7b4e15 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 2 Mar 2022 01:22:15 +0100 Subject: [PATCH 065/165] Add missing temperature sensor for Shelly Motion2 (#67458) --- homeassistant/components/shelly/sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index ce9c57f5889..21a7447e2b2 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -215,6 +215,15 @@ SENSORS: Final = { icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, ), + ("sensor", "temp"): BlockSensorDescription( + key="sensor|temp", + name="Temperature", + unit_fn=temperature_unit, + value=lambda value: round(value, 1), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ("sensor", "extTemp"): BlockSensorDescription( key="sensor|extTemp", name="Temperature", From 1ebb4cf395a99724dbe544a01894f0a36486672d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 17:21:51 -0800 Subject: [PATCH 066/165] Bumped version to 2022.3.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 e46509d2b84..51af5c06d22 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 = 3 -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 58a3918d0ad..4d686e63c5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b5 +version = 2022.3.0b6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 4c0ba7cd77204fa5025c89a84c83dea427d0af93 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Mar 2022 16:49:48 +0100 Subject: [PATCH 067/165] Improve mobile_app key handling (#67429) --- homeassistant/components/mobile_app/const.py | 1 + .../components/mobile_app/helpers.py | 74 ++++-- .../components/mobile_app/webhook.py | 25 +- tests/components/mobile_app/test_http_api.py | 44 ++++ tests/components/mobile_app/test_webhook.py | 225 +++++++++++++++++- 5 files changed, 344 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index a2a4e15ee72..ba81a0484cf 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -28,6 +28,7 @@ ATTR_CONFIG_ENTRY_ID = "entry_id" ATTR_DEVICE_NAME = "device_name" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" +ATTR_NO_LEGACY_ENCRYPTION = "no_legacy_encryption" ATTR_OS_NAME = "os_name" ATTR_OS_VERSION = "os_version" ATTR_PUSH_WEBSOCKET_CHANNEL = "push_websocket_channel" diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index b7d38357a78..545c3511fc9 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -7,7 +7,7 @@ import json import logging from aiohttp.web import Response, json_response -from nacl.encoding import Base64Encoder +from nacl.encoding import Base64Encoder, HexEncoder, RawEncoder from nacl.secret import SecretBox from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON @@ -23,6 +23,7 @@ from .const import ( ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_SECRET, @@ -34,7 +35,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def setup_decrypt() -> tuple[int, Callable]: +def setup_decrypt(key_encoder) -> tuple[int, Callable]: """Return decryption function and length of key. Async friendly. @@ -42,12 +43,14 @@ def setup_decrypt() -> tuple[int, Callable]: def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return SecretBox(key, encoder=key_encoder).decrypt( + ciphertext, encoder=Base64Encoder + ) return (SecretBox.KEY_SIZE, decrypt) -def setup_encrypt() -> tuple[int, Callable]: +def setup_encrypt(key_encoder) -> tuple[int, Callable]: """Return encryption function and length of key. Async friendly. @@ -55,15 +58,22 @@ def setup_encrypt() -> tuple[int, Callable]: def encrypt(ciphertext, key): """Encrypt ciphertext using key.""" - return SecretBox(key).encrypt(ciphertext, encoder=Base64Encoder) + return SecretBox(key, encoder=key_encoder).encrypt( + ciphertext, encoder=Base64Encoder + ) return (SecretBox.KEY_SIZE, encrypt) -def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: +def _decrypt_payload_helper( + key: str | None, + ciphertext: str, + get_key_bytes: Callable[[str, int], str | bytes], + key_encoder, +) -> dict[str, str] | None: """Decrypt encrypted payload.""" try: - keylen, decrypt = setup_decrypt() + keylen, decrypt = setup_decrypt(key_encoder) except OSError: _LOGGER.warning("Ignoring encrypted payload because libsodium not installed") return None @@ -72,18 +82,33 @@ def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: _LOGGER.warning("Ignoring encrypted payload because no decryption key known") return None - key_bytes = key.encode("utf-8") - key_bytes = key_bytes[:keylen] - key_bytes = key_bytes.ljust(keylen, b"\0") + key_bytes = get_key_bytes(key, keylen) - try: - msg_bytes = decrypt(ciphertext, key_bytes) - message = json.loads(msg_bytes.decode("utf-8")) - _LOGGER.debug("Successfully decrypted mobile_app payload") - return message - except ValueError: - _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") - return None + msg_bytes = decrypt(ciphertext, key_bytes) + message = json.loads(msg_bytes.decode("utf-8")) + _LOGGER.debug("Successfully decrypted mobile_app payload") + return message + + +def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: + """Decrypt encrypted payload.""" + + def get_key_bytes(key: str, keylen: int) -> str: + return key + + return _decrypt_payload_helper(key, ciphertext, get_key_bytes, HexEncoder) + + +def _decrypt_payload_legacy(key: str | None, ciphertext: str) -> dict[str, str] | None: + """Decrypt encrypted payload.""" + + def get_key_bytes(key: str, keylen: int) -> bytes: + key_bytes = key.encode("utf-8") + key_bytes = key_bytes[:keylen] + key_bytes = key_bytes.ljust(keylen, b"\0") + return key_bytes + + return _decrypt_payload_helper(key, ciphertext, get_key_bytes, RawEncoder) def registration_context(registration: dict) -> Context: @@ -158,11 +183,16 @@ def webhook_response( data = json.dumps(data, cls=JSONEncoder) if registration[ATTR_SUPPORTS_ENCRYPTION]: - keylen, encrypt = setup_encrypt() + keylen, encrypt = setup_encrypt( + HexEncoder if ATTR_NO_LEGACY_ENCRYPTION in registration else RawEncoder + ) - key = registration[CONF_SECRET].encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b"\0") + if ATTR_NO_LEGACY_ENCRYPTION in registration: + key = registration[CONF_SECRET] + else: + key = registration[CONF_SECRET].encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b"\0") enc_data = encrypt(data.encode("utf-8"), key).decode("utf-8") data = json.dumps({"encrypted": True, "encrypted_data": enc_data}) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 221c4eef733..860b8ef7b53 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -7,6 +7,7 @@ import logging import secrets from aiohttp.web import HTTPBadRequest, Request, Response, json_response +from nacl.exceptions import CryptoError from nacl.secret import SecretBox import voluptuous as vol @@ -58,6 +59,7 @@ from .const import ( ATTR_EVENT_TYPE, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, @@ -97,6 +99,7 @@ from .const import ( ) from .helpers import ( _decrypt_payload, + _decrypt_payload_legacy, empty_okay_response, error_response, registration_context, @@ -191,7 +194,27 @@ async def handle_webhook( if req_data[ATTR_WEBHOOK_ENCRYPTED]: enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] - webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) + try: + webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) + if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data: + data = {**config_entry.data, ATTR_NO_LEGACY_ENCRYPTION: True} + hass.config_entries.async_update_entry(config_entry, data=data) + except CryptoError: + if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data: + try: + webhook_payload = _decrypt_payload_legacy( + config_entry.data[CONF_SECRET], enc_data + ) + except CryptoError: + _LOGGER.warning( + "Ignoring encrypted payload because unable to decrypt" + ) + except ValueError: + _LOGGER.warning("Ignoring invalid encrypted payload") + else: + _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") + except ValueError: + _LOGGER.warning("Ignoring invalid encrypted payload") if webhook_type not in WEBHOOK_COMMANDS: _LOGGER.error( diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 5d92418bba2..4c4e9b54ccf 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -1,4 +1,5 @@ """Tests for the mobile_app HTTP API.""" +from binascii import unhexlify from http import HTTPStatus import json from unittest.mock import patch @@ -75,6 +76,49 @@ async def test_registration_encryption(hass, hass_client): assert resp.status == HTTPStatus.CREATED register_json = await resp.json() + key = unhexlify(register_json[CONF_SECRET]) + + payload = json.dumps(RENDER_TEMPLATE["data"]).encode("utf-8") + + data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await api_client.post( + f"/api/webhook/{register_json[CONF_WEBHOOK_ID]}", json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = SecretBox(key).decrypt( + webhook_json["encrypted_data"], encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + assert json.loads(decrypted_data) == {"one": "Hello world"} + + +async def test_registration_encryption_legacy(hass, hass_client): + """Test that registrations happen.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + api_client = await hass_client() + + resp = await api_client.post("/api/mobile_app/registrations", json=REGISTER) + + assert resp.status == HTTPStatus.CREATED + register_json = await resp.json() + keylen = SecretBox.KEY_SIZE key = register_json[CONF_SECRET].encode("utf-8") key = key[:keylen] diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 48b61988de2..5f220cf0ebe 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,5 @@ """Webhook tests for mobile_app.""" +from binascii import unhexlify from http import HTTPStatus from unittest.mock import patch @@ -22,7 +23,29 @@ from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE from tests.common import async_mock_service -def encrypt_payload(secret_key, payload): +def encrypt_payload(secret_key, payload, encode_json=True): + """Return a encrypted payload given a key and dictionary of data.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + prepped_key = unhexlify(secret_key) + + if encode_json: + payload = json.dumps(payload) + payload = payload.encode("utf-8") + + return ( + SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + ) + + +def encrypt_payload_legacy(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" try: from nacl.encoding import Base64Encoder @@ -38,7 +61,9 @@ def encrypt_payload(secret_key, payload): prepped_key = prepped_key[:keylen] prepped_key = prepped_key.ljust(keylen, b"\0") - payload = json.dumps(payload).encode("utf-8") + if encode_json: + payload = json.dumps(payload) + payload = payload.encode("utf-8") return ( SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") @@ -56,6 +81,27 @@ def decrypt_payload(secret_key, encrypted_data): import json + prepped_key = unhexlify(secret_key) + + decrypted_data = SecretBox(prepped_key).decrypt( + encrypted_data, encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + return json.loads(decrypted_data) + + +def decrypt_payload_legacy(secret_key, encrypted_data): + """Return a decrypted payload given a key and a string of encrypted data.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] @@ -273,6 +319,181 @@ async def test_webhook_handle_decryption(webhook_client, create_registrations): assert decrypted_data == {"one": "Hello world"} +async def test_webhook_handle_decryption_legacy(webhook_client, create_registrations): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + +async def test_webhook_handle_decryption_fail( + webhook_client, create_registrations, caplog +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send valid data + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + assert decrypted_data == {"one": "Hello world"} + caplog.clear() + + # Send invalid JSON data + data = encrypt_payload(key, "{not_valid", encode_json=False) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring invalid encrypted payload" in caplog.text + caplog.clear() + + # Break the key, and send JSON data + data = encrypt_payload(key[::-1], RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring encrypted payload because unable to decrypt" in caplog.text + + +async def test_webhook_handle_decryption_legacy_fail( + webhook_client, create_registrations, caplog +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send valid data using legacy method + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + assert decrypted_data == {"one": "Hello world"} + caplog.clear() + + # Send invalid JSON data + data = encrypt_payload_legacy(key, "{not_valid", encode_json=False) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload_legacy(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring invalid encrypted payload" in caplog.text + caplog.clear() + + # Break the key, and send JSON data + data = encrypt_payload_legacy(key[::-1], RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload_legacy(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring encrypted payload because unable to decrypt" in caplog.text + + +async def test_webhook_handle_decryption_legacy_upgrade( + webhook_client, create_registrations +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send using legacy method + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + # Send using new method + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + # Send using legacy method - no longer possible + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + # The response should be empty, encrypted with the new method + with pytest.raises(Exception): + decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {} + + async def test_webhook_requires_encryption(webhook_client, create_registrations): """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( From c81ccaebd34ec13160f8a786325ae3181ab6d0b7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 2 Mar 2022 10:36:23 +0100 Subject: [PATCH 068/165] Rfxtrx correct overzealous type checking (#67437) --- homeassistant/components/rfxtrx/config_flow.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 7a842ad470c..549a5c3ccbf 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -34,7 +34,13 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from . import DOMAIN, DeviceTuple, get_device_id, get_rfx_object +from . import ( + DOMAIN, + DeviceTuple, + get_device_id, + get_device_tuple_from_identifiers, + get_rfx_object, +) from .binary_sensor import supported as binary_supported from .const import ( CONF_AUTOMATIC_ADD, @@ -59,7 +65,7 @@ CONF_MANUAL_PATH = "Enter Manually" class DeviceData(TypedDict): """Dict data representing a device entry.""" - event_code: str + event_code: str | None device_id: DeviceTuple @@ -388,15 +394,15 @@ class OptionsFlow(config_entries.OptionsFlow): def _get_device_data(self, entry_id) -> DeviceData: """Get event code based on device identifier.""" - event_code: str + event_code: str | None = None entry = self._device_registry.async_get(entry_id) assert entry - device_id = cast(DeviceTuple, next(iter(entry.identifiers))[1:]) + device_id = get_device_tuple_from_identifiers(entry.identifiers) + assert device_id for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id: event_code = cast(str, packet_id) break - assert event_code return DeviceData(event_code=event_code, device_id=device_id) @callback From 94fd7ec028e31095609a56f2a4f100e79a4b9107 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Mar 2022 16:53:13 +0100 Subject: [PATCH 069/165] Improve binary sensor group when member is unknown or unavailable (#67468) --- .../components/group/binary_sensor.py | 14 +++++++++-- tests/components/group/test_binary_sensor.py | 24 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 9c0301d97e6..de0c3d393ca 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -80,7 +81,6 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} self._attr_unique_id = unique_id self._device_class = device_class - self._state: str | None = None self.mode = any if mode: self.mode = all @@ -106,13 +106,23 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): def async_update_group_state(self) -> None: """Query all members and determine the binary sensor group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] + + # filtered_states are members currently in the state machine filtered_states: list[str] = [x.state for x in all_states if x is not None] + + # Set group as unavailable if all members are unavailable self._attr_available = any( state != STATE_UNAVAILABLE for state in filtered_states ) - if STATE_UNAVAILABLE in filtered_states: + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + ) + if not valid_state: + # Set as unknown if any / all member is not unknown or unavailable self._attr_is_on = None else: + # Set as ON if any / all member is ON states = list(map(lambda x: x == STATE_ON, filtered_states)) state = self.mode(states) self._attr_is_on = state diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 0a85c793aaa..a0872b11f16 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -95,6 +95,16 @@ async def test_state_reporting_all(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + async def test_state_reporting_any(hass): """Test the state reporting.""" @@ -116,11 +126,10 @@ async def test_state_reporting_any(hass): await hass.async_start() await hass.async_block_till_done() - # binary sensors have state off if unavailable hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_OFF) @@ -137,7 +146,6 @@ async def test_state_reporting_any(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - # binary sensors have state off if unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -149,3 +157,13 @@ async def test_state_reporting_any(hass): entry = entity_registry.async_get("binary_sensor.binary_sensor_group") assert entry assert entry.unique_id == "unique_identifier" + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN From 274e4d5558cb9873b597049133c334777d239d2e Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 2 Mar 2022 12:58:39 +0000 Subject: [PATCH 070/165] Bump to aiohomekit 0.7.15 (#67470) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 44de321db4a..dfd45991b3f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.14"], + "requirements": ["aiohomekit==0.7.15"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 4d6dcefb10f..91afd8a8e55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -184,7 +184,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.14 +aiohomekit==0.7.15 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7828110fc2..0d228b65601 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.14 +aiohomekit==0.7.15 # homeassistant.components.emulated_hue # homeassistant.components.http From 4668720f0265339a6ca026dde52f6471f0930fa6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 2 Mar 2022 14:00:48 +0000 Subject: [PATCH 071/165] Remove Ecobee homekit vendor extensions that just don't work (#67474) --- .../components/homekit_controller/number.py | 36 --------- .../specific_devices/test_ecobee3.py | 80 ------------------- 2 files changed, 116 deletions(-) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 5fcb5027640..b994bc80f4a 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -48,42 +48,6 @@ NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), - CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_COOL: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_COOL, - name="Home Cool Target", - icon="mdi:thermometer-minus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_HEAT: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_HEAT, - name="Home Heat Target", - icon="mdi:thermometer-plus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_COOL: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_COOL, - name="Sleep Cool Target", - icon="mdi:thermometer-minus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_HEAT: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_HEAT, - name="Sleep Heat Target", - icon="mdi:thermometer-plus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_COOL: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_COOL, - name="Away Cool Target", - icon="mdi:thermometer-minus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_HEAT: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_HEAT, - name="Away Heat Target", - icon="mdi:thermometer-plus", - entity_category=EntityCategory.CONFIG, - ), } diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 83378650b97..3c47195b442 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -14,12 +14,10 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.number import NumberMode from homeassistant.components.sensor import SensorStateClass from homeassistant.config_entries import ConfigEntryState from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import EntityCategory from tests.components.homekit_controller.common import ( HUB_TEST_ACCESSORY_ID, @@ -123,84 +121,6 @@ async def test_ecobee3_setup(hass): }, state="heat", ), - EntityTestInfo( - entity_id="number.homew_home_cool_target", - friendly_name="HomeW Home Cool Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:35", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 33.3, - "min": 18.3, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="24.4", - ), - EntityTestInfo( - entity_id="number.homew_home_heat_target", - friendly_name="HomeW Home Heat Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:34", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 26.1, - "min": 7.2, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="22.2", - ), - EntityTestInfo( - entity_id="number.homew_sleep_cool_target", - friendly_name="HomeW Sleep Cool Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:37", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 33.3, - "min": 18.3, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="27.8", - ), - EntityTestInfo( - entity_id="number.homew_sleep_heat_target", - friendly_name="HomeW Sleep Heat Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:36", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 26.1, - "min": 7.2, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="17.8", - ), - EntityTestInfo( - entity_id="number.homew_away_cool_target", - friendly_name="HomeW Away Cool Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:39", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 33.3, - "min": 18.3, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="26.7", - ), - EntityTestInfo( - entity_id="number.homew_away_heat_target", - friendly_name="HomeW Away Heat Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:38", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 26.1, - "min": 7.2, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="18.9", - ), EntityTestInfo( entity_id="sensor.homew_current_temperature", friendly_name="HomeW Current Temperature", From 9aba0ba990be6c1a8bbc609f8fe8b98848241202 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Thu, 3 Mar 2022 02:54:47 +1100 Subject: [PATCH 072/165] Sort DMS results using only criteria supported by the device (#67475) --- homeassistant/components/dlna_dms/dms.py | 23 +++++- .../dlna_dms/test_dms_device_source.py | 77 ++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 1166d1db6db..d7ee08f85f8 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -542,7 +542,7 @@ class DmsDeviceSource: children = await self._device.async_browse_direct_children( object_id, metadata_filter=DLNA_BROWSE_FILTER, - sort_criteria=DLNA_SORT_CRITERIA, + sort_criteria=self._sort_criteria, ) return self._didl_to_media_source(base_object, children) @@ -675,6 +675,27 @@ class DmsDeviceSource: """Make an identifier for BrowseMediaSource.""" return f"{self.source_id}/{action}{object_id}" + @property # type: ignore + @functools.cache + def _sort_criteria(self) -> list[str]: + """Return criteria to be used for sorting results. + + The device must be connected before reading this property. + """ + assert self._device + + if self._device.sort_capabilities == ["*"]: + return DLNA_SORT_CRITERIA + + # Filter criteria based on what the device supports. Strings in + # DLNA_SORT_CRITERIA are prefixed with a sign, while those in + # the device's sort_capabilities are not. + return [ + criterion + for criterion in DLNA_SORT_CRITERIA + if criterion[1:] in self._device.sort_capabilities + ] + class Action(StrEnum): """Actions that can be specified in a DMS media-source identifier.""" diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 4ee9cce91ba..d6fcdb267d6 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -8,7 +8,7 @@ from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite import pytest -from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.const import DLNA_SORT_CRITERIA, DOMAIN from homeassistant.components.dlna_dms.dms import ( ActionError, DeviceConnectionError, @@ -686,6 +686,81 @@ async def test_browse_media_object( assert not child.children +async def test_browse_object_sort_anything( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sort criteria for children where device allows anything.""" + dms_device_mock.sort_capabilities = ["*"] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be dlna_dms's default + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=DLNA_SORT_CRITERIA + ) + + +async def test_browse_object_sort_superset( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sorting where device allows superset of integration's criteria.""" + dms_device_mock.sort_capabilities = [ + "dc:title", + "upnp:originalTrackNumber", + "upnp:class", + "upnp:artist", + "dc:creator", + "upnp:genre", + ] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be dlna_dms's default + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=DLNA_SORT_CRITERIA + ) + + +async def test_browse_object_sort_subset( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sorting where device allows subset of integration's criteria.""" + dms_device_mock.sort_capabilities = [ + "dc:title", + "upnp:class", + ] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be reduced to only those allowed, + # and in the order specified by DLNA_SORT_CRITERIA + expected_criteria = ["+upnp:class", "+dc:title"] + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=expected_criteria + ) + + async def test_browse_media_path( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: From 092b9730679051c35920242cdd5fd1f21c8abeaf Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 2 Mar 2022 14:30:54 +0200 Subject: [PATCH 073/165] Bump aioshelly to 1.0.11 (#67476) --- 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 1e9f34608eb..1d4d47748be 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.10"], + "requirements": ["aioshelly==1.0.11"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 91afd8a8e55..9714786a873 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.10 +aioshelly==1.0.11 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d228b65601..8f539520713 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.10 +aioshelly==1.0.11 # homeassistant.components.steamist aiosteamist==0.3.1 From 288270ac0879a2ca745f7c167489714c7284b308 Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 2 Mar 2022 13:00:33 +0100 Subject: [PATCH 074/165] Address late review of flipr (#67477) --- homeassistant/components/flipr/__init__.py | 3 +-- tests/components/flipr/test_binary_sensor.py | 3 ++- tests/components/flipr/test_sensor.py | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 1a9f3dc0314..3281410ec2d 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -75,8 +75,7 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): self.client.get_pool_measure_latest, self.flipr_id ) except (FliprError) as error: - _LOGGER.error(error) - raise UpdateFailed from error + raise UpdateFailed(error) from error return data diff --git a/tests/components/flipr/test_binary_sensor.py b/tests/components/flipr/test_binary_sensor.py index 48f9361723c..fc24ddee340 100644 --- a/tests/components/flipr/test_binary_sensor.py +++ b/tests/components/flipr/test_binary_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as entity_reg from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -36,7 +37,7 @@ async def test_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 45816801472..c5ab3dc1541 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as entity_reg from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", @@ -102,7 +103,7 @@ async def test_error_flipr_api_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", From da4f4f641d4fc9dc3db2a46943925a2d5e3bfd91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Mar 2022 08:22:34 -0800 Subject: [PATCH 075/165] Add guard radio browser media source (#67486) --- .../components/radio_browser/media_source.py | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index 8240691b247..6ba1b7b2b9a 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, ) from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -35,9 +36,8 @@ async def async_get_media_source(hass: HomeAssistant) -> RadioMediaSource: """Set up Radio Browser media source.""" # Radio browser support only a single config entry entry = hass.config_entries.async_entries(DOMAIN)[0] - radios = hass.data[DOMAIN] - return RadioMediaSource(hass, radios, entry) + return RadioMediaSource(hass, entry) class RadioMediaSource(MediaSource): @@ -45,26 +45,33 @@ class RadioMediaSource(MediaSource): name = "Radio Browser" - def __init__( - self, hass: HomeAssistant, radios: RadioBrowser, entry: ConfigEntry - ) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize CameraMediaSource.""" super().__init__(DOMAIN) self.hass = hass self.entry = entry - self.radios = radios + + @property + def radios(self) -> RadioBrowser | None: + """Return the radio browser.""" + return self.hass.data.get(DOMAIN) async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve selected Radio station to a streaming URL.""" - station = await self.radios.station(uuid=item.identifier) + radios = self.radios + + if radios is None: + raise Unresolvable("Radio Browser not initialized") + + station = await radios.station(uuid=item.identifier) if not station: - raise BrowseError("Radio station is no longer available") + raise Unresolvable("Radio station is no longer available") if not (mime_type := self._async_get_station_mime_type(station)): - raise BrowseError("Could not determine stream type of radio station") + raise Unresolvable("Could not determine stream type of radio station") # Register "click" with Radio Browser - await self.radios.station_click(uuid=station.uuid) + await radios.station_click(uuid=station.uuid) return PlayMedia(station.url, mime_type) @@ -73,6 +80,11 @@ class RadioMediaSource(MediaSource): item: MediaSourceItem, ) -> BrowseMediaSource: """Return media.""" + radios = self.radios + + if radios is None: + raise BrowseError("Radio Browser not initialized") + return BrowseMediaSource( domain=DOMAIN, identifier=None, @@ -83,10 +95,10 @@ class RadioMediaSource(MediaSource): can_expand=True, children_media_class=MEDIA_CLASS_DIRECTORY, children=[ - *await self._async_build_popular(item), - *await self._async_build_by_tag(item), - *await self._async_build_by_language(item), - *await self._async_build_by_country(item), + *await self._async_build_popular(radios, item), + *await self._async_build_by_tag(radios, item), + *await self._async_build_by_language(radios, item), + *await self._async_build_by_country(radios, item), ], ) @@ -100,7 +112,9 @@ class RadioMediaSource(MediaSource): return mime_type @callback - def _async_build_stations(self, stations: list[Station]) -> list[BrowseMediaSource]: + def _async_build_stations( + self, radios: RadioBrowser, stations: list[Station] + ) -> list[BrowseMediaSource]: """Build list of media sources from radio stations.""" items: list[BrowseMediaSource] = [] @@ -126,23 +140,23 @@ class RadioMediaSource(MediaSource): return items async def _async_build_by_country( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing radio stations by country.""" category, _, country_code = (item.identifier or "").partition("/") if country_code: - stations = await self.radios.stations( + stations = await radios.stations( filter_by=FilterBy.COUNTRY_CODE_EXACT, filter_term=country_code, hide_broken=True, order=Order.NAME, reverse=False, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) # We show country in the root additionally, when there is no item if not item.identifier or category == "country": - countries = await self.radios.countries(order=Order.NAME) + countries = await radios.countries(order=Order.NAME) return [ BrowseMediaSource( domain=DOMAIN, @@ -160,22 +174,22 @@ class RadioMediaSource(MediaSource): return [] async def _async_build_by_language( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing radio stations by language.""" category, _, language = (item.identifier or "").partition("/") if category == "language" and language: - stations = await self.radios.stations( + stations = await radios.stations( filter_by=FilterBy.LANGUAGE_EXACT, filter_term=language, hide_broken=True, order=Order.NAME, reverse=False, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) if category == "language": - languages = await self.radios.languages(order=Order.NAME, hide_broken=True) + languages = await radios.languages(order=Order.NAME, hide_broken=True) return [ BrowseMediaSource( domain=DOMAIN, @@ -206,17 +220,17 @@ class RadioMediaSource(MediaSource): return [] async def _async_build_popular( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing popular radio stations.""" if item.identifier == "popular": - stations = await self.radios.stations( + stations = await radios.stations( hide_broken=True, limit=250, order=Order.CLICK_COUNT, reverse=True, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) if not item.identifier: return [ @@ -234,22 +248,22 @@ class RadioMediaSource(MediaSource): return [] async def _async_build_by_tag( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing radio stations by tags.""" category, _, tag = (item.identifier or "").partition("/") if category == "tag" and tag: - stations = await self.radios.stations( + stations = await radios.stations( filter_by=FilterBy.TAG_EXACT, filter_term=tag, hide_broken=True, order=Order.NAME, reverse=False, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) if category == "tag": - tags = await self.radios.tags( + tags = await radios.tags( hide_broken=True, limit=100, order=Order.STATION_COUNT, From ddf7efd93759fb70426dc08bf3e111ee340ee61a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Mar 2022 18:18:58 +0100 Subject: [PATCH 076/165] Bumped version to 2022.3.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 51af5c06d22..c0ff111fc89 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 = 3 -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 4d686e63c5b..d72829b574e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0b6 +version = 2022.3.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 9db56a8119c82cda49ceabbeb63f93a7e335e148 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Feb 2022 00:14:12 +0100 Subject: [PATCH 077/165] Don't trigger device removal for non rfxtrx devices (#67315) --- homeassistant/components/rfxtrx/__init__.py | 33 ++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 2dcfe639a64..8dda4d32644 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -6,7 +6,7 @@ import binascii from collections.abc import Callable import copy import logging -from typing import NamedTuple +from typing import NamedTuple, cast import RFXtrx as rfxtrxmod import async_timeout @@ -229,11 +229,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): devices[device_id] = config @callback - def _remove_device(event: Event): - if event.data["action"] != "remove": - return - device_entry = device_registry.deleted_devices[event.data["device_id"]] - device_id = next(iter(device_entry.identifiers))[1:] + def _remove_device(device_id: DeviceTuple): data = { **entry.data, CONF_DEVICES: { @@ -245,8 +241,19 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): hass.config_entries.async_update_entry(entry=entry, data=data) devices.pop(device_id) + @callback + def _updated_device(event: Event): + if event.data["action"] != "remove": + return + device_entry = device_registry.deleted_devices[event.data["device_id"]] + if entry.entry_id not in device_entry.config_entries: + return + device_id = get_device_tuple_from_identifiers(device_entry.identifiers) + if device_id: + _remove_device(device_id) + entry.async_on_unload( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, _remove_device) + hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) ) def _shutdown_rfxtrx(event): @@ -413,6 +420,18 @@ def get_device_id( return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string) +def get_device_tuple_from_identifiers( + identifiers: set[tuple[str, str]] +) -> DeviceTuple | None: + """Calculate the device tuple from a device entry.""" + identifier = next((x for x in identifiers if x[0] == DOMAIN), None) + if not identifier: + return None + # work around legacy identifier, being a multi tuple value + identifier2 = cast(tuple[str, str, str, str], identifier) + return DeviceTuple(identifier2[1], identifier2[2], identifier2[3]) + + async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry ) -> bool: From b9f44eec0a006adde40fb773799858ca72e0a9a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:56:48 +0100 Subject: [PATCH 078/165] Bump docker/login-action from 1.13.0 to 1.14.0 (#67416) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index ce545684da5..a62ef8fb2d7 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -122,13 +122,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -187,13 +187,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -252,13 +252,13 @@ jobs: uses: actions/checkout@v2.4.0 - name: Login to DockerHub - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From be19a2e2ab439ce38766a1a0a875be3f7a9d835d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 08:48:40 +0100 Subject: [PATCH 079/165] Bump docker/login-action from 1.14.0 to 1.14.1 (#67462) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index a62ef8fb2d7..26811dae73e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -122,13 +122,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -187,13 +187,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -252,13 +252,13 @@ jobs: uses: actions/checkout@v2.4.0 - name: Login to DockerHub - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} From 0349d7d09d93bed2641decf60ee8a44b1c20e1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 2 Mar 2022 13:55:05 +0100 Subject: [PATCH 080/165] Split meta image creation (#67480) --- .github/workflows/builder.yml | 104 ++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 26811dae73e..a857ef77edb 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -243,21 +243,28 @@ jobs: channel: beta publish_container: - name: Publish meta container + name: Publish meta container for ${{ matrix.registry }} if: github.repository_owner == 'home-assistant' needs: ["init", "build_base"] runs-on: ubuntu-latest + strategy: + matrix: + registry: + - "ghcr.io/home-assistant" + - "homeassistant" steps: - name: Checkout the repository uses: actions/checkout@v2.4.0 - name: Login to DockerHub + if: matrix.registry == 'homeassistant' uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry + if: matrix.registry == 'ghcr.io/home-assistant' uses: docker/login-action@v1.14.1 with: registry: ghcr.io @@ -273,38 +280,37 @@ jobs: export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { - local docker_reg=${1} - local tag_l=${2} - local tag_r=${3} + local tag_l=${1} + local tag_r=${2} - docker manifest create "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/amd64-homeassistant:${tag_r}" \ - "${docker_reg}/i386-homeassistant:${tag_r}" \ - "${docker_reg}/armhf-homeassistant:${tag_r}" \ - "${docker_reg}/armv7-homeassistant:${tag_r}" \ - "${docker_reg}/aarch64-homeassistant:${tag_r}" + docker manifest create "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/i386-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/amd64-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \ --os linux --arch amd64 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/i386-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/i386-homeassistant:${tag_r}" \ --os linux --arch 386 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/armhf-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \ --os linux --arch arm --variant=v6 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/armv7-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \ --os linux --arch arm --variant=v7 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/aarch64-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" \ --os linux --arch arm64 --variant=v8 - docker manifest push --purge "${docker_reg}/home-assistant:${tag_l}" + docker manifest push --purge "${{ matrix.registry }}/home-assistant:${tag_l}" } function validate_image() { @@ -315,36 +321,34 @@ jobs: fi } - for docker_reg in "homeassistant" "ghcr.io/home-assistant"; do - docker pull "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}" - # Create version tag - create_manifest "${docker_reg}" "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" + # Create version tag + create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" - # Create general tags - if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then - create_manifest "${docker_reg}" "dev" "${{ needs.init.outputs.version }}" - elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then - create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" - else - create_manifest "${docker_reg}" "stable" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + create_manifest"dev" "${{ needs.init.outputs.version }}" + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + create_manifest "beta" "${{ needs.init.outputs.version }}" + create_manifest "rc" "${{ needs.init.outputs.version }}" + else + create_manifest "stable" "${{ needs.init.outputs.version }}" + create_manifest "latest" "${{ needs.init.outputs.version }}" + create_manifest "beta" "${{ needs.init.outputs.version }}" + create_manifest "rc" "${{ needs.init.outputs.version }}" - # Create series version tag (e.g. 2021.6) - v="${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "${v%.*}" "${{ needs.init.outputs.version }}" - fi - done + # Create series version tag (e.g. 2021.6) + v="${{ needs.init.outputs.version }}" + create_manifest "${v%.*}" "${{ needs.init.outputs.version }}" + fi From d7c480f2d8bd15efca17b04a37428c046b1e2b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 2 Mar 2022 16:55:33 +0100 Subject: [PATCH 081/165] Set fail-fast to false for meta container (#67484) --- .github/workflows/builder.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index a857ef77edb..2ca9b754a3a 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -248,6 +248,7 @@ jobs: needs: ["init", "build_base"] runs-on: ubuntu-latest strategy: + fail-fast: false matrix: registry: - "ghcr.io/home-assistant" From 48d9e9a83ce9f4d75386fc9251c358bf98b5508b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Mar 2022 16:41:03 -0600 Subject: [PATCH 082/165] Bump soco to 0.26.4 (#67498) --- 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 4bb8623acb2..6f482e92dc9 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.3"], + "requirements": ["soco==0.26.4"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 9714786a873..bf18ddba431 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2235,7 +2235,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.3 +soco==0.26.4 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f539520713..d32cd1609b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1371,7 +1371,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.3 +soco==0.26.4 # homeassistant.components.solaredge solaredge==0.0.2 From ee0bdaa2dea1031bd120a762e7e97cc97155ecd1 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 00:41:50 +0100 Subject: [PATCH 083/165] Check if UPnP is enabled on Fritz device (#67512) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/fritz/__init__.py | 6 ++++++ homeassistant/components/fritz/common.py | 10 ++++++++++ homeassistant/components/fritz/config_flow.py | 7 +++++++ homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/strings.json | 1 + homeassistant/components/fritz/translations/en.json | 5 +++-- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index a0e0413366b..0b334ff616a 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except FRITZ_EXCEPTIONS as ex: raise ConfigEntryNotReady from ex + if ( + "X_AVM-DE_UPnP1" in avm_wrapper.connection.services + and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"] + ): + raise ConfigEntryAuthFailed("Missing UPnP configuration") + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = avm_wrapper diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index b2a429bfa3c..2fc28433e56 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -630,6 +630,11 @@ class AvmWrapper(FritzBoxTools): ) return {} + async def async_get_upnp_configuration(self) -> dict[str, Any]: + """Call X_AVM-DE_UPnP service.""" + + return await self.hass.async_add_executor_job(self.get_upnp_configuration) + async def async_get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" @@ -698,6 +703,11 @@ class AvmWrapper(FritzBoxTools): partial(self.set_allow_wan_access, ip_address, turn_on) ) + def get_upnp_configuration(self) -> dict[str, Any]: + """Call X_AVM-DE_UPnP service.""" + + return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo") + def get_ontel_num_deflections(self) -> dict[str, Any]: """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 0844d725522..046f00ba3a9 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -29,6 +29,7 @@ from .const import ( ERROR_AUTH_INVALID, ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, + ERROR_UPNP_NOT_CONFIGURED, ) _LOGGER = logging.getLogger(__name__) @@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") return ERROR_UNKNOWN + if ( + "X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services + and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"] + ): + return ERROR_UPNP_NOT_CONFIGURED + return None async def async_check_configured_entry(self) -> ConfigEntry | None: diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index f33cf463996..f739ccf6858 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -46,6 +46,7 @@ DEFAULT_USERNAME = "" ERROR_AUTH_INVALID = "invalid_auth" ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured" ERROR_UNKNOWN = "unknown_error" FRITZ_SERVICES = "fritz_services" diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 450566f101b..a65b2900f66 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -36,6 +36,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "upnp_not_configured": "Missing UPnP settings on device.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 0a58ee686f3..c6fa4a16036 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,7 +9,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "upnp_not_configured": "Missing UPnP settings on device." }, "flow_title": "{name}", "step": { @@ -51,4 +52,4 @@ } } } -} \ No newline at end of file +} From 63f8e9ee08747301df60ca2a37c177fd88b83470 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Mar 2022 21:40:15 +0100 Subject: [PATCH 084/165] Fix MQTT config flow with advanced parameters (#67556) * Fix MQTT config flow with advanced parameters * Add test --- homeassistant/components/mqtt/__init__.py | 106 +++++++++++-------- homeassistant/components/mqtt/config_flow.py | 29 ++--- homeassistant/components/mqtt/const.py | 7 ++ tests/components/mqtt/test_config_flow.py | 97 ++++++++++++++++- 4 files changed, 177 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 107bc4660c2..c6229c2d475 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -75,11 +75,16 @@ from .const import ( ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, DATA_MQTT_CONFIG, @@ -94,6 +99,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + PROTOCOL_31, PROTOCOL_311, ) from .discovery import LAST_DISCOVERY @@ -118,13 +124,6 @@ SERVICE_DUMP = "dump" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_KEEPALIVE = "keepalive" -CONF_CERTIFICATE = "certificate" -CONF_CLIENT_KEY = "client_key" -CONF_CLIENT_CERT = "client_cert" -CONF_TLS_INSECURE = "tls_insecure" -CONF_TLS_VERSION = "tls_version" - -PROTOCOL_31 = "3.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 @@ -757,6 +756,58 @@ class Subscription: encoding: str | None = attr.ib(default="utf-8") +class MqttClientSetup: + """Helper class to setup the paho mqtt client from config.""" + + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper.""" + + if config[CONF_PROTOCOL] == PROTOCOL_31: + proto = self.mqtt.MQTTv31 + else: + proto = self.mqtt.MQTTv311 + + if (client_id := config.get(CONF_CLIENT_ID)) is None: + # PAHO MQTT relies on the MQTT server to generate random client IDs. + # However, that feature is not mandatory so we generate our own. + client_id = self.mqtt.base62(uuid.uuid4().int, padding=22) + self._client = self.mqtt.Client(client_id, protocol=proto) + + # Enable logging + self._client.enable_logger() + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if username is not None: + self._client.username_pw_set(username, password) + + if (certificate := config.get(CONF_CERTIFICATE)) == "auto": + certificate = certifi.where() + + client_key = config.get(CONF_CLIENT_KEY) + client_cert = config.get(CONF_CLIENT_CERT) + tls_insecure = config.get(CONF_TLS_INSECURE) + if certificate is not None: + self._client.tls_set( + certificate, + certfile=client_cert, + keyfile=client_key, + tls_version=ssl.PROTOCOL_TLS, + ) + + if tls_insecure is not None: + self._client.tls_insecure_set(tls_insecure) + + @property + def client(self) -> mqtt.Client: + """Return the paho MQTT client.""" + return self._client + + class MQTT: """Home Assistant MQTT client.""" @@ -821,46 +872,7 @@ class MQTT: def init_client(self): """Initialize paho client.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - if self.conf[CONF_PROTOCOL] == PROTOCOL_31: - proto: int = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - if (client_id := self.conf.get(CONF_CLIENT_ID)) is None: - # PAHO MQTT relies on the MQTT server to generate random client IDs. - # However, that feature is not mandatory so we generate our own. - client_id = mqtt.base62(uuid.uuid4().int, padding=22) - self._mqttc = mqtt.Client(client_id, protocol=proto) - - # Enable logging - self._mqttc.enable_logger() - - username = self.conf.get(CONF_USERNAME) - password = self.conf.get(CONF_PASSWORD) - if username is not None: - self._mqttc.username_pw_set(username, password) - - if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto": - certificate = certifi.where() - - client_key = self.conf.get(CONF_CLIENT_KEY) - client_cert = self.conf.get(CONF_CLIENT_CERT) - tls_insecure = self.conf.get(CONF_TLS_INSECURE) - if certificate is not None: - self._mqttc.tls_set( - certificate, - certfile=client_cert, - keyfile=client_key, - tls_version=ssl.PROTOCOL_TLS, - ) - - if tls_insecure is not None: - self._mqttc.tls_insecure_set(tls_insecure) - + self._mqttc = MqttClientSetup(self.conf).client self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_message = self._mqtt_on_message diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 3f93e50829a..99e7e9718d0 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult +from . import MqttClientSetup from .const import ( ATTR_PAYLOAD, ATTR_QOS, @@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = self._hassio_discovery can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, data[CONF_HOST], data[CONF_PORT], data.get(CONF_USERNAME), @@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -313,25 +317,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) -def try_connection(broker, port, username, password, protocol="3.1"): +def try_connection(hass, broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if protocol == "3.1": - proto = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - client = mqtt.Client(protocol=proto) - if username and password: - client.username_pw_set(username, password) + # Get the config from configuration.yaml + yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) + entry_config = { + CONF_BROKER: broker, + CONF_PORT: port, + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_PROTOCOL: protocol, + } + client = MqttClientSetup({**yaml_config, **entry_config}).client result = queue.Queue(maxsize=1) def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" - result.put(result_code == mqtt.CONNACK_ACCEPTED) + result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED) client.on_connect = on_connect diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index f04348ee002..69865733763 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" CONF_WILL_MESSAGE = "will_message" +CONF_CERTIFICATE = "certificate" +CONF_CLIENT_KEY = "client_key" +CONF_CLIENT_CERT = "client_cert" +CONF_TLS_INSECURE = "tls_insecure" +CONF_TLS_VERSION = "tls_version" + DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" @@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" +PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index d9aab02e821..88c6137bf94 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3,8 +3,9 @@ from unittest.mock import patch import pytest import voluptuous as vol +import yaml -from homeassistant import config_entries, data_entry_flow +from homeassistant import config as hass_config, config_entries, data_entry_flow from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo from homeassistant.core import HomeAssistant @@ -151,7 +152,7 @@ async def test_manual_config_set( "discovery": True, } # Check we tried the connection, with precedence for config entry settings - mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None) + mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None) # Check config entry got setup assert len(mock_finish_setup.mock_calls) == 1 @@ -642,3 +643,95 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection): mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PORT: 1234, } + + +async def test_try_connection_with_advanced_parameters( + hass, mock_try_connection_success, tmp_path +): + """Test config flow with advanced parameters from config.""" + # Mock certificate files + certfile = tmp_path / "cert.pem" + certfile.write_text("## mock certificate file ##") + keyfile = tmp_path / "key.pem" + keyfile.write_text("## mock key file ##") + config = { + "certificate": "auto", + "tls_insecure": True, + "client_cert": certfile, + "client_key": keyfile, + } + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({mqtt.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 async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + config_entry.data = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/online", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 1, + mqtt.ATTR_RETAIN: True, + }, + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/offline", + mqtt.ATTR_PAYLOAD: "offline", + mqtt.ATTR_QOS: 2, + mqtt.ATTR_RETAIN: False, + }, + } + + # Test default/suggested values from config + 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"] == "broker" + defaults = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + } + suggested = { + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + } + for k, v in defaults.items(): + assert get_default(result["data_schema"].schema, k) == v + for k, v in suggested.items(): + assert get_suggested(result["data_schema"].schema, k) == v + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "another-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "us3r", + mqtt.CONF_PASSWORD: "p4ss", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + + # check if the username and password was set from config flow and not from configuration.yaml + assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == ( + "us3r", + "p4ss", + ) + + # check if tls_insecure_set is called + assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,) + + # check if the certificate settings were set from configuration.yaml + assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ + "certfile" + ] == str(certfile) + assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ + "keyfile" + ] == str(keyfile) From eff7a12557f53b95f23b3cd88eadc10146d0b6ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Mar 2022 15:03:03 -0800 Subject: [PATCH 085/165] Highlight in logs it is a custom component when setup fails (#67559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/setup.py | 18 +++++++++++++----- tests/test_setup.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 36292989dce..2599b4b3c85 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -149,10 +149,17 @@ async def _async_setup_component( This method is a coroutine. """ + integration: loader.Integration | None = None - def log_error(msg: str, link: str | None = None) -> None: + def log_error(msg: str) -> None: """Log helper.""" - _LOGGER.error("Setup failed for %s: %s", domain, msg) + if integration is None: + custom = "" + link = None + else: + custom = "" if integration.is_built_in else "custom integration " + link = integration.documentation + _LOGGER.error("Setup failed for %s%s: %s", custom, domain, msg) async_notify_setup_error(hass, domain, link) try: @@ -174,7 +181,7 @@ async def _async_setup_component( try: await async_process_deps_reqs(hass, config, integration) except HomeAssistantError as err: - log_error(str(err), integration.documentation) + log_error(str(err)) return False # Some integrations fail on import because they call functions incorrectly. @@ -182,7 +189,7 @@ async def _async_setup_component( try: component = integration.get_component() except ImportError as err: - log_error(f"Unable to import component: {err}", integration.documentation) + log_error(f"Unable to import component: {err}") return False processed_config = await conf_util.async_process_component_config( @@ -190,7 +197,7 @@ async def _async_setup_component( ) if processed_config is None: - log_error("Invalid config.", integration.documentation) + log_error("Invalid config.") return False start = timer() @@ -287,6 +294,7 @@ async def async_prepare_setup_platform( def log_error(msg: str) -> None: """Log helper.""" + _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) async_notify_setup_error(hass, platform_path) diff --git a/tests/test_setup.py b/tests/test_setup.py index f71ba01410b..04924344c2b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, @@ -621,6 +622,22 @@ async def test_integration_disabled(hass, caplog): assert disabled_reason in caplog.text +async def test_integration_logs_is_custom(hass, caplog): + """Test we highlight it's a custom component when errors happen.""" + mock_integration( + hass, + MockModule("test_component1"), + built_in=False, + ) + with patch( + "homeassistant.setup.async_process_deps_reqs", + side_effect=HomeAssistantError("Boom"), + ): + result = await setup.async_setup_component(hass, "test_component1", {}) + assert not result + assert "Setup failed for custom integration test_component1: Boom" in caplog.text + + async def test_async_get_loaded_integrations(hass): """Test we can enumerate loaded integations.""" hass.config.components.add("notbase") From d36164350011290b28446bc30c4d8021c49997a8 Mon Sep 17 00:00:00 2001 From: Emory Penney Date: Thu, 3 Mar 2022 15:22:36 -0800 Subject: [PATCH 086/165] Bump pyobihai (#67571) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index f908ad16179..96f803cebdd 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -2,7 +2,7 @@ "domain": "obihai", "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": ["pyobihai==1.3.1"], + "requirements": ["pyobihai==1.3.2"], "codeowners": ["@dshokouhi"], "iot_class": "local_polling", "loggers": ["pyobihai"] diff --git a/requirements_all.txt b/requirements_all.txt index bf18ddba431..84f1dbb2004 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1727,7 +1727,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.3.1 +pyobihai==1.3.2 # homeassistant.components.octoprint pyoctoprintapi==0.1.7 From b5b945ab4d97d6f3441a39943d1ef0c48e20f922 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Thu, 3 Mar 2022 23:05:13 +0000 Subject: [PATCH 087/165] Fix data type for growatt lastdataupdate (#67511) (#67582) Co-authored-by: Paulus Schoutsen --- homeassistant/components/growatt_server/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 71c1c69e08a..67095492de7 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -221,12 +221,9 @@ class GrowattData: # Create datetime from the latest entry date_now = dt.now().date() last_updated_time = dt.parse_time(str(sorted_keys[-1])) - combined_timestamp = datetime.datetime.combine( + mix_detail["lastdataupdate"] = datetime.datetime.combine( date_now, last_updated_time ) - # Convert datetime to UTC - combined_timestamp_utc = dt.as_utc(combined_timestamp) - mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat() # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined # imported from grid value that is the combination of charging AND load consumption From 73765a1f2914ab2401dd86e2763e0d7c9df40996 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Mar 2022 13:03:46 -1000 Subject: [PATCH 088/165] Add guards for HomeKit version/names that break apple watches (#67585) --- .../components/homekit/accessories.py | 8 +++-- homeassistant/components/homekit/util.py | 31 +++++++++++++++---- tests/components/homekit/test_accessories.py | 17 +++++++--- tests/components/homekit/test_type_sensors.py | 2 +- tests/components/homekit/test_util.py | 15 +++++++-- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d348b4c1f42..4129c3225b7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -274,7 +274,7 @@ class HomeAccessory(Accessory): if self.config.get(ATTR_SW_VERSION) is not None: sw_version = format_version(self.config[ATTR_SW_VERSION]) if sw_version is None: - sw_version = __version__ + sw_version = format_version(__version__) hw_version = None if self.config.get(ATTR_HW_VERSION) is not None: hw_version = format_version(self.config[ATTR_HW_VERSION]) @@ -289,7 +289,9 @@ class HomeAccessory(Accessory): serv_info = self.get_service(SERV_ACCESSORY_INFO) char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION) serv_info.add_characteristic(char) - serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version) + serv_info.configure_char( + CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH] + ) self.iid_manager.assign(char) char.broker = self @@ -532,7 +534,7 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(driver, name) self.set_info_service( - firmware_revision=__version__, + firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER, diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8c64b9b0443..7fa4ffa8bf6 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__) NUMBERS_ONLY_RE = re.compile(r"[^\d.]+") VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?") +MAX_VERSION_PART = 2**32 - 1 MAX_PORT = 65535 @@ -363,7 +364,15 @@ def convert_to_float(state): return None -def cleanup_name_for_homekit(name: str | None) -> str | None: +def coerce_int(state: str) -> int: + """Return int.""" + try: + return int(state) + except (ValueError, TypeError): + return 0 + + +def cleanup_name_for_homekit(name: str | None) -> str: """Ensure the name of the device will not crash homekit.""" # # This is not a security measure. @@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None: # UNICODE_EMOJI is also not allowed but that # likely isn't a problem if name is None: - return None + return "None" # None crashes apple watches return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] @@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): ) +def _format_version_part(version_part: str) -> str: + return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part)))) + + def format_version(version): """Extract the version string in a format homekit can consume.""" - split_ver = str(version).replace("-", ".") + split_ver = str(version).replace("-", ".").replace(" ", ".") num_only = NUMBERS_ONLY_RE.sub("", split_ver) - if match := VERSION_RE.search(num_only): - return match.group(0) - return None + if (match := VERSION_RE.search(num_only)) is None: + return None + value = ".".join(map(_format_version_part, match.group(0).split("."))) + return None if _is_zero_but_true(value) else value + + +def _is_zero_but_true(value): + """Zero but true values can crash apple watches.""" + return convert_to_float(value) == 0 def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 103ee9ea2da..704bb368d64 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -42,7 +42,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - __version__, __version__ as hass_version, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS @@ -166,7 +165,9 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) hass.states.async_set(entity_id, "on") await hass.async_block_till_done() @@ -216,7 +217,9 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert isinstance(acc.to_HAP(), dict) @@ -244,7 +247,9 @@ async def test_accessory_with_hardware_revision(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" assert isinstance(acc.to_HAP(), dict) @@ -687,7 +692,9 @@ def test_home_bridge(hk_driver): serv = bridge.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index d864a90fe61..9b6d1c9cee2 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver): assert acc.category == 10 # Sensor assert acc.char_humidity.value == 20 - assert acc.display_name is None + assert acc.display_name == "None" diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0432fb27426..3dd30af2056 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import ( async_port_is_available, async_show_setup_message, cleanup_name_for_homekit, + coerce_int, convert_to_float, density_to_air_quality, format_version, @@ -349,13 +350,23 @@ async def test_format_version(): assert format_version("undefined-undefined-1.6.8") == "1.6.8" assert format_version("56.0-76060") == "56.0.76060" assert format_version(3.6) == "3.6" - assert format_version("AK001-ZJ100") == "001.100" + assert format_version("AK001-ZJ100") == "1.100" assert format_version("HF-LPB100-") == "100" - assert format_version("AK001-ZJ2149") == "001.2149" + assert format_version("AK001-ZJ2149") == "1.2149" + assert format_version("13216407885") == "4294967295" # max value + assert format_version("000132 16407885") == "132.16407885" assert format_version("0.1") == "0.1" + assert format_version("0") is None assert format_version("unknown") is None +async def test_coerce_int(): + """Test coerce_int method.""" + assert coerce_int("1") == 1 + assert coerce_int("") == 0 + assert coerce_int(0) == 0 + + async def test_accessory_friendly_name(): """Test we provide a helpful friendly name.""" From ba40d62081eb81f5101a6c74ac04a4859f8e54cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Mar 2022 15:53:54 -0800 Subject: [PATCH 089/165] Bumped version to 2022.3.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 c0ff111fc89..9c546af49f8 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 = 3 -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 d72829b574e..1d731bfdb9c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0 +version = 2022.3.1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 8bbf55c85d5c78fb8cd25bbb70906ca43ce47b56 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 23:43:33 +0100 Subject: [PATCH 090/165] Add unique_id to Fritz diagnostics (#67384) Co-authored-by: Paulus Schoutsen --- homeassistant/components/fritz/diagnostics.py | 3 +++ tests/components/fritz/test_diagnostics.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index fa4ff6a7db8..ed45295892b 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -22,6 +22,9 @@ async def async_get_config_entry_diagnostics( "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": { "model": avm_wrapper.model, + "unique_id": avm_wrapper.unique_id.replace( + avm_wrapper.unique_id[6:11], "XX:XX" + ), "current_firmware": avm_wrapper.current_firmware, "latest_firmware": avm_wrapper.latest_firmware, "update_available": avm_wrapper.update_available, diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index 892210d0844..a4b4942c375 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import MOCK_USER_DATA +from .const import MOCK_MESH_MASTER_MAC, MOCK_USER_DATA from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -69,6 +69,7 @@ async def test_entry_diagnostics( "latest_firmware": None, "mesh_role": "master", "model": "FRITZ!Box 7530 AX", + "unique_id": MOCK_MESH_MASTER_MAC.replace("6F:12", "XX:XX"), "update_available": False, "wan_link_properties": { "NewLayer1DownstreamMaxBitRate": 318557000, From 9849b86a8455fefe01d09f3ff72c9b153a1bc587 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Mar 2022 23:08:29 -0600 Subject: [PATCH 091/165] Suppress roku power off timeout errors (#67414) --- homeassistant/components/roku/__init__.py | 32 --------------- homeassistant/components/roku/helpers.py | 40 +++++++++++++++++++ homeassistant/components/roku/manifest.json | 2 +- homeassistant/components/roku/media_player.py | 29 +++++++------- homeassistant/components/roku/remote.py | 8 ++-- homeassistant/components/roku/select.py | 5 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roku/test_media_player.py | 9 ++++- 9 files changed, 70 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index e6e31f08713..f24d08909b8 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,14 +1,6 @@ """Support for Roku.""" from __future__ import annotations -from collections.abc import Awaitable, Callable, Coroutine -from functools import wraps -import logging -from typing import Any, TypeVar - -from rokuecp import RokuConnectionError, RokuError -from typing_extensions import Concatenate, ParamSpec - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant @@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator -from .entity import RokuEntity CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -27,10 +18,6 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, ] -_LOGGER = logging.getLogger(__name__) - -_T = TypeVar("_T", bound="RokuEntity") -_P = ParamSpec("_P") async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -def roku_exception_handler( - func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc] -) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc] - """Decorate Roku calls to handle Roku exceptions.""" - - @wraps(func) - async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: - try: - await func(self, *args, **kwargs) - except RokuConnectionError as error: - if self.available: - _LOGGER.error("Error communicating with API: %s", error) - except RokuError as error: - if self.available: - _LOGGER.error("Invalid response from API: %s", error) - - return wrapper diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py index 7f507a9fe52..26fdb53c935 100644 --- a/homeassistant/components/roku/helpers.py +++ b/homeassistant/components/roku/helpers.py @@ -1,6 +1,21 @@ """Helpers for Roku.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +import logging +from typing import Any, TypeVar + +from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError +from typing_extensions import Concatenate, ParamSpec + +from .entity import RokuEntity + +_LOGGER = logging.getLogger(__name__) + +_T = TypeVar("_T", bound=RokuEntity) +_P = ParamSpec("_P") + def format_channel_name(channel_number: str, channel_name: str | None = None) -> str: """Format a Roku Channel name.""" @@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) -> return f"{channel_name} ({channel_number})" return channel_number + + +def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]: + """Decorate Roku calls to handle Roku exceptions.""" + + def decorator( + func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc] + ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc] + @wraps(func) + async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + await func(self, *args, **kwargs) + except RokuConnectionTimeoutError as error: + if not ignore_timeout and self.available: + _LOGGER.error("Error communicating with API: %s", error) + except RokuConnectionError as error: + if self.available: + _LOGGER.error("Error communicating with API: %s", error) + except RokuError as error: + if self.available: + _LOGGER.error("Invalid response from API: %s", error) + + return wrapper + + return decorator diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 4918e7742be..433ce6b29d1 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.14.1"], + "requirements": ["rokuecp==0.15.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 9cf17d890a4..8dd76f0b9cb 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import roku_exception_handler from .browse_media import async_browse_media from .const import ( ATTR_ARTIST_NAME, @@ -65,7 +64,7 @@ from .const import ( ) from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity -from .helpers import format_channel_name +from .helpers import format_channel_name, roku_exception_handler _LOGGER = logging.getLogger(__name__) @@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): app.name for app in self.coordinator.data.apps if app.name is not None ) - @roku_exception_handler + @roku_exception_handler() async def search(self, keyword: str) -> None: """Emulate opening the search screen and entering the search keyword.""" await self.coordinator.roku.search(keyword) @@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): media_content_type, ) - @roku_exception_handler + @roku_exception_handler() async def async_turn_on(self) -> None: """Turn on the Roku.""" await self.coordinator.roku.remote("poweron") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler(ignore_timeout=True) async def async_turn_off(self) -> None: """Turn off the Roku.""" await self.coordinator.roku.remote("poweroff") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_pause(self) -> None: """Send pause command.""" if self.state not in (STATE_STANDBY, STATE_PAUSED): await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_play(self) -> None: """Send play command.""" if self.state not in (STATE_STANDBY, STATE_PLAYING): await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_play_pause(self) -> None: """Send play/pause command.""" if self.state != STATE_STANDBY: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.coordinator.roku.remote("reverse") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_next_track(self) -> None: """Send next track command.""" await self.coordinator.roku.remote("forward") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self.coordinator.roku.remote("volume_mute") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_volume_up(self) -> None: """Volume up media player.""" await self.coordinator.roku.remote("volume_up") - @roku_exception_handler + @roku_exception_handler() async def async_volume_down(self) -> None: """Volume down media player.""" await self.coordinator.roku.remote("volume_down") - @roku_exception_handler + @roku_exception_handler() async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: @@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_select_source(self, source: str) -> None: """Select input source.""" if source == "Home": diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 9a0cd6f51e3..6d1312c0b03 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import roku_exception_handler from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity +from .helpers import roku_exception_handler async def async_setup_entry( @@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity): """Return true if device is on.""" return not self.coordinator.data.state.standby - @roku_exception_handler + @roku_exception_handler() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.coordinator.roku.remote("poweron") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler(ignore_timeout=True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.coordinator.roku.remote("poweroff") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] diff --git a/homeassistant/components/roku/select.py b/homeassistant/components/roku/select.py index 9120a4fe9ce..e11748114d1 100644 --- a/homeassistant/components/roku/select.py +++ b/homeassistant/components/roku/select.py @@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import roku_exception_handler from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity -from .helpers import format_channel_name +from .helpers import format_channel_name, roku_exception_handler @dataclass @@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity): """Return a set of selectable options.""" return self.entity_description.options_fn(self.coordinator.data) - @roku_exception_handler + @roku_exception_handler() async def async_select_option(self, option: str) -> None: """Set the option.""" await self.entity_description.set_fn( diff --git a/requirements_all.txt b/requirements_all.txt index 84f1dbb2004..debbd81d853 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2121,7 +2121,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.14.1 +rokuecp==0.15.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d32cd1609b5..92688ce8908 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ rflink==0.0.62 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.14.1 +rokuecp==0.15.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 050814e3817..21fd2e861b6 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import MagicMock, patch import pytest -from rokuecp import RokuError +from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( @@ -164,10 +164,15 @@ async def test_tv_setup( assert device_entry.suggested_area == "Living room" +@pytest.mark.parametrize( + "error", + [RokuConnectionTimeoutError, RokuConnectionError, RokuError], +) async def test_availability( hass: HomeAssistant, mock_roku: MagicMock, mock_config_entry: MockConfigEntry, + error: RokuError, ) -> None: """Test entity availability.""" now = dt_util.utcnow() @@ -179,7 +184,7 @@ async def test_availability( await hass.async_block_till_done() with patch("homeassistant.util.dt.utcnow", return_value=future): - mock_roku.update.side_effect = RokuError + mock_roku.update.side_effect = error async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE From 24013ad94cf26a459dcd646e3aa835abf4c3d65c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 3 Mar 2022 11:29:58 +0100 Subject: [PATCH 092/165] rfxtrx: bump to 0.28 (#67530) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index d7125518329..edbf5c8556c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.27.1"], + "requirements": ["pyRFXtrx==0.28.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index debbd81d853..695842fe244 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1360,7 +1360,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.27.1 +pyRFXtrx==0.28.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92688ce8908..37b96d4f8ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.27.1 +pyRFXtrx==0.28.0 # homeassistant.components.tibber pyTibber==0.22.1 From b54652a849783340d71d8d8724b3bc60d0a8410b Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 4 Mar 2022 05:55:01 +0100 Subject: [PATCH 093/165] Remove use of deprecated xiaomi_miio classes (#67590) --- homeassistant/components/xiaomi_miio/__init__.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 2849b249762..7fb489b3467 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -14,7 +14,6 @@ from miio import ( AirHumidifierMiot, AirHumidifierMjjsq, AirPurifier, - AirPurifierMB4, AirPurifierMiot, CleaningDetails, CleaningSummary, @@ -23,10 +22,8 @@ from miio import ( DNDStatus, Fan, Fan1C, + FanMiot, FanP5, - FanP9, - FanP10, - FanP11, FanZA5, RoborockVacuum, Timer, @@ -52,7 +49,6 @@ from .const import ( KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, - MODEL_AIRPURIFIER_3C, MODEL_FAN_1C, MODEL_FAN_P5, MODEL_FAN_P9, @@ -111,10 +107,10 @@ AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR] MODEL_TO_CLASS_MAP = { MODEL_FAN_1C: Fan1C, - MODEL_FAN_P10: FanP10, - MODEL_FAN_P11: FanP11, + MODEL_FAN_P9: FanMiot, + MODEL_FAN_P10: FanMiot, + MODEL_FAN_P11: FanMiot, MODEL_FAN_P5: FanP5, - MODEL_FAN_P9: FanP9, MODEL_FAN_ZA5: FanZA5, } @@ -314,8 +310,6 @@ async def async_create_miio_device_and_coordinator( device = AirHumidifier(host, token, model=model) migrate = True # Airpurifiers and Airfresh - elif model == MODEL_AIRPURIFIER_3C: - device = AirPurifierMB4(host, token) elif model in MODELS_PURIFIER_MIOT: device = AirPurifierMiot(host, token) elif model.startswith("zhimi.airpurifier."): From 679ddbd1bed99b207e673d15bcd966d884af58ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 4 Mar 2022 10:03:38 +0100 Subject: [PATCH 094/165] Downgrade Renault warning (#67601) Co-authored-by: epenet --- homeassistant/components/renault/renault_vehicle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 12860bc6b9a..2d15e9c14a3 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -104,7 +104,7 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.warning( + LOGGER.info( "Ignoring endpoint %s as it is not supported for this vehicle: %s", coordinator.name, coordinator.last_exception, @@ -112,7 +112,7 @@ class RenaultVehicleProxy: del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.warning( + LOGGER.info( "Ignoring endpoint %s as it is denied for this vehicle: %s", coordinator.name, coordinator.last_exception, From b290e6217063d3f53a0662dd49e97c0464c864dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Mar 2022 12:30:40 -1000 Subject: [PATCH 095/165] Handle elkm1 login case with username and insecure login (#67602) --- homeassistant/components/elkm1/__init__.py | 22 ++++++++++--------- homeassistant/components/elkm1/config_flow.py | 4 +--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 04a26f2822b..6791c2ec1bb 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -279,9 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: keypad.add_callback(_element_changed) try: - if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME]) - ): + if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT): return False except asyncio.TimeoutError as exc: raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc @@ -334,7 +332,6 @@ async def async_wait_for_elk_to_sync( elk: elkm1.Elk, login_timeout: int, sync_timeout: int, - password_auth: bool, ) -> bool: """Wait until the elk has finished sync. Can fail login or timeout.""" @@ -354,18 +351,23 @@ async def async_wait_for_elk_to_sync( login_event.set() sync_event.set() + def first_response(*args, **kwargs): + _LOGGER.debug("ElkM1 received first response (VN)") + login_event.set() + def sync_complete(): sync_event.set() success = True elk.add_handler("login", login_status) + # VN is the first command sent for panel, when we get + # it back we now we are logged in either with or without a password + elk.add_handler("VN", first_response) elk.add_handler("sync_complete", sync_complete) - events = [] - if password_auth: - events.append(("login", login_event, login_timeout)) - events.append(("sync_complete", sync_event, sync_timeout)) - - for name, event, timeout in events: + for name, event, timeout in ( + ("login", login_event, login_timeout), + ("sync_complete", sync_event, sync_timeout), + ): _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) try: async with async_timeout.timeout(timeout): diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index a21cf186005..96f9fd5d078 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -81,9 +81,7 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid) - ): + if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT): raise InvalidAuth short_mac = _short_mac(mac) if mac else None From 5657a9e6bd44100105ad4387a23a99f78358fafb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 4 Mar 2022 19:20:10 +0100 Subject: [PATCH 096/165] Fix sql false warning (#67614) --- homeassistant/components/sql/sensor.py | 2 +- tests/components/sql/test_sensor.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 1c8514d0d26..0a240469f83 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -172,7 +172,7 @@ class SQLSensor(SensorEntity): else: self._attr_native_value = data - if not data: + if data is None: _LOGGER.warning("%s returned no results", self._query) sess.close() diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 0e543f98a21..05f49d553e9 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -115,7 +115,9 @@ async def test_query_limit(hass: HomeAssistant) -> None: assert state.attributes["value"] == 5 -async def test_query_no_value(hass: HomeAssistant) -> None: +async def test_query_no_value( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test the SQL sensor with a query that returns no value.""" config = { "sensor": { @@ -137,6 +139,9 @@ async def test_query_no_value(hass: HomeAssistant) -> None: state = hass.states.get("sensor.count_tables") assert state.state == STATE_UNKNOWN + text = "SELECT 5 as value where 1=2 returned no results" + assert text in caplog.text + async def test_invalid_query(hass: HomeAssistant) -> None: """Test the SQL sensor for invalid queries.""" From 5ae83e3c4093477e224a22dd3cf0279b0f3f396f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 23:38:28 +0100 Subject: [PATCH 097/165] Allign logic for Fritz sensors and binary_sensors (#67623) --- .../components/fritz/binary_sensor.py | 13 ++++++---- homeassistant/components/fritz/common.py | 25 ++++++++++++++++++ homeassistant/components/fritz/sensor.py | 26 +++---------------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index b416e0cfb11..db1aac99c47 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -1,6 +1,7 @@ """AVM FRITZ!Box connectivity sensor.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging @@ -14,8 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .common import AvmWrapper, FritzBoxBaseEntity -from .const import DOMAIN, MeshRoles +from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) class FritzBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Fritz sensor entity.""" - exclude_mesh_role: MeshRoles = MeshRoles.SLAVE + is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = ( @@ -45,7 +46,7 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = ( name="Firmware Update", device_class=BinarySensorDeviceClass.UPDATE, entity_category=EntityCategory.DIAGNOSTIC, - exclude_mesh_role=MeshRoles.NONE, + is_suitable=lambda info: True, ), ) @@ -57,10 +58,12 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box binary sensors") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + connection_info = await avm_wrapper.async_get_connection_info() + entities = [ FritzBoxBinarySensor(avm_wrapper, entry.title, description) for description in SENSOR_TYPES - if (description.exclude_mesh_role != avm_wrapper.mesh_role) + if description.is_suitable(connection_info) ] async_add_entities(entities, True) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 2fc28433e56..4c307c126cd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -642,6 +642,22 @@ class AvmWrapper(FritzBoxTools): partial(self.get_wan_link_properties) ) + async def async_get_connection_info(self) -> ConnectionInfo: + """Return ConnectionInfo data.""" + + link_properties = await self.async_get_wan_link_properties() + connection_info = ConnectionInfo( + connection=link_properties.get("NewWANAccessType", "").lower(), + mesh_role=self.mesh_role, + wan_enabled=self.device_is_router, + ) + _LOGGER.debug( + "ConnectionInfo for FritzBox %s: %s", + self.host, + connection_info, + ) + return connection_info + async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" @@ -970,3 +986,12 @@ class FritzBoxBaseEntity: name=self._device_name, sw_version=self._avm_wrapper.current_firmware, ) + + +@dataclass +class ConnectionInfo: + """Fritz sensor connection information class.""" + + connection: str + mesh_role: MeshRoles + wan_enabled: bool diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index f01966d7114..9811adf6829 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -28,8 +28,8 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from .common import AvmWrapper, FritzBoxBaseEntity -from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION, MeshRoles +from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity +from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) @@ -134,15 +134,6 @@ def _retrieve_link_attenuation_received_state( return status.attenuation[1] / 10 # type: ignore[no-any-return] -@dataclass -class ConnectionInfo: - """Fritz sensor connection information class.""" - - connection: str - mesh_role: MeshRoles - wan_enabled: bool - - @dataclass class FritzRequireKeysMixin: """Fritz sensor data class.""" @@ -283,18 +274,7 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box sensors") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - link_properties = await avm_wrapper.async_get_wan_link_properties() - connection_info = ConnectionInfo( - connection=link_properties.get("NewWANAccessType", "").lower(), - mesh_role=avm_wrapper.mesh_role, - wan_enabled=avm_wrapper.device_is_router, - ) - - _LOGGER.debug( - "ConnectionInfo for FritzBox %s: %s", - avm_wrapper.host, - connection_info, - ) + connection_info = await avm_wrapper.async_get_connection_info() entities = [ FritzBoxSensor(avm_wrapper, entry.title, description) From 2a6d5ea7bd16f2e1b9ba131fd3effb2f225bc8d0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 15:49:22 +0100 Subject: [PATCH 098/165] Improve logging for Fritz switches creation (#67640) --- homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/switch.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index f739ccf6858..a3ba907366c 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -57,6 +57,7 @@ SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password" SWITCH_TYPE_DEFLECTION = "CallDeflection" SWITCH_TYPE_PORTFORWARD = "PortForward" +SWITCH_TYPE_PROFILE = "Profile" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 730ffb7fc0d..cac6e735a81 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -30,6 +30,7 @@ from .const import ( DOMAIN, SWITCH_TYPE_DEFLECTION, SWITCH_TYPE_PORTFORWARD, + SWITCH_TYPE_PROFILE, SWITCH_TYPE_WIFINETWORK, WIFI_STANDARD, MeshRoles, @@ -185,6 +186,7 @@ def profile_entities_list( data_fritz: FritzData, ) -> list[FritzBoxProfileSwitch]: """Add new tracker entities from the AVM device.""" + _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PROFILE) new_profiles: list[FritzBoxProfileSwitch] = [] @@ -198,11 +200,15 @@ def profile_entities_list( if device_filter_out_from_trackers( mac, device, data_fritz.profile_switches.values() ): + _LOGGER.debug( + "Skipping profile switch creation for device %s", device.hostname + ) continue new_profiles.append(FritzBoxProfileSwitch(avm_wrapper, device)) data_fritz.profile_switches[avm_wrapper.unique_id].add(mac) + _LOGGER.debug("Creating %s profile switches", len(new_profiles)) return new_profiles From d7348718e02612e73ffbb7e363513fd1d8d05dcb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Mar 2022 20:17:11 +0100 Subject: [PATCH 099/165] Fix Fan template loosing percentage/preset (#67648) Co-authored-by: J. Nick Koston --- homeassistant/components/template/fan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 1ddd37ba7bc..1d25c24017f 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -277,8 +277,6 @@ class TemplateFan(TemplateEntity, FanEntity): """Turn off the fan.""" await self._off_script.async_run(context=self._context) self._state = STATE_OFF - self._percentage = 0 - self._preset_mode = None async def async_set_percentage(self, percentage: int) -> None: """Set the percentage speed of the fan.""" From f3c85b3459a3ad20ad0b2059ef2483ce577ccf33 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 23:17:43 +0100 Subject: [PATCH 100/165] Fix reload of media player groups (#67653) --- homeassistant/components/group/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index b36d4f1033f..8e595d75db6 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -59,11 +59,12 @@ SERVICE_SET = "set" SERVICE_REMOVE = "remove" PLATFORMS = [ - Platform.LIGHT, - Platform.COVER, - Platform.NOTIFY, - Platform.FAN, Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.MEDIA_PLAYER, + Platform.NOTIFY, ] REG_KEY = f"{DOMAIN}_registry" From f5aaf44e50c6277c4f1705ec0fe742271a9a6e1c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Mar 2022 20:09:49 +0100 Subject: [PATCH 101/165] Bump pydroid-ipcam to 1.3.1 (#67655) * Bump pydroid-ipcam to 1.3.1 * Remove loop and set ssl to False --- homeassistant/components/android_ip_webcam/__init__.py | 2 +- homeassistant/components/android_ip_webcam/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index ca4af7fd68a..67bb00f441d 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -204,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Init ip webcam cam = PyDroidIPCam( - hass.loop, websession, host, cam_config[CONF_PORT], username=username, password=password, timeout=cam_config[CONF_TIMEOUT], + ssl=False, ) if switches is None: diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 637a773ac33..39223e6636d 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -2,7 +2,7 @@ "domain": "android_ip_webcam", "name": "Android IP Webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", - "requirements": ["pydroid-ipcam==0.8"], + "requirements": ["pydroid-ipcam==1.3.1"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 695842fe244..6447f33b2c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1486,7 +1486,7 @@ pydispatcher==2.0.5 pydoods==1.0.2 # homeassistant.components.android_ip_webcam -pydroid-ipcam==0.8 +pydroid-ipcam==1.3.1 # homeassistant.components.ebox pyebox==1.1.4 From 4f8b69d985d239cf5bf36d2747141cbf2b4cbb39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Mar 2022 22:02:22 -1000 Subject: [PATCH 102/165] Ensure elkm1 can be manually configured when discovered instance is not used (#67712) --- homeassistant/components/elkm1/config_flow.py | 14 +- tests/components/elkm1/test_config_flow.py | 153 +++++++++++++++++- 2 files changed, 161 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 96f9fd5d078..b8cd89edae4 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -225,7 +225,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(user_input, self.unique_id) except asyncio.TimeoutError: - return {CONF_HOST: "cannot_connect"}, None + return {"base": "cannot_connect"}, None except InvalidAuth: return {CONF_PASSWORD: "invalid_auth"}, None except Exception: # pylint: disable=broad-except @@ -285,9 +285,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if device := await async_discover_device( self.hass, user_input[CONF_ADDRESS] ): - await self.async_set_unique_id(dr.format_mac(device.mac_address)) + await self.async_set_unique_id( + dr.format_mac(device.mac_address), raise_on_progress=False + ) self._abort_if_unique_id_configured() - user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}" + # Ignore the port from discovery since its always going to be + # 2601 if secure is turned on even though they may want insecure + user_input[CONF_ADDRESS] = device.ip_address errors, result = await self._async_create_or_error(user_input, False) if not errors: return result @@ -322,7 +326,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if is_ip_address(host) and ( device := await async_discover_device(self.hass, host) ): - await self.async_set_unique_id(dr.format_mac(device.mac_address)) + await self.async_set_unique_id( + dr.format_mac(device.mac_address), raise_on_progress=False + ) self._abort_if_unique_id_configured() return (await self._async_create_or_error(user_input, True))[1] diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 49402d7b4d5..183ab90086c 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -73,6 +73,155 @@ async def test_form_user_with_secure_elk_no_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_user_with_insecure_elk_skip_discovery(hass): + """Test we can setup a insecure elk with skipping discovery.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_insecure_elk_no_discovery(hass): + """Test we can setup a insecure elk.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_insecure_elk_times_out(hass): + """Test we can setup a insecure elk that times out.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=False) + + with patch( + "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", + 0, + ), patch( + "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0 + ), _patch_discovery(), _patch_elk( + elk=mocked_elk + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass): """Test we abort when we try to configure the same ip.""" config_entry = MockConfigEntry( @@ -262,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, - "host": "elks://127.0.0.1:2601", + "host": "elks://127.0.0.1", "password": "test-password", "prefix": "", "username": "test-username", @@ -434,7 +583,7 @@ async def test_form_cannot_connect(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {CONF_HOST: "cannot_connect"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_unknown_exception(hass): From 92c3c08a106bb40e6a6202f920f922661a2be879 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Mar 2022 22:02:45 -1000 Subject: [PATCH 103/165] Add missing disconnect in elkm1 config flow validation (#67716) --- homeassistant/components/elkm1/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index b8cd89edae4..d68fce268a2 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -81,8 +81,11 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT): - raise InvalidAuth + try: + if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT): + raise InvalidAuth + finally: + elk.disconnect() short_mac = _short_mac(mac) if mac else None if prefix and prefix != short_mac: From 10a2c97cab4ac83e35f0a0ec38e467f8c63a1d65 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 6 Mar 2022 19:03:52 +1100 Subject: [PATCH 104/165] Update aiolifx dependency to resolve log flood (#67721) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index b034745ee31..9251bcb1f50 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"], "homekit": { "models": ["LIFX"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 6447f33b2c5..973cfb19c59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aiokafka==0.6.0 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.7.0 +aiolifx==0.7.1 # homeassistant.components.lifx aiolifx_effects==0.2.2 From 88e0380aa21b2de7a6b668831bde7089c509ac89 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Mar 2022 00:07:45 -0800 Subject: [PATCH 105/165] Bumped version to 2022.3.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 9c546af49f8..e91d5cf3725 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 = 3 -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 1d731bfdb9c..e9a6dbb8ee5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.1 +version = 2022.3.2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 76336df91acd09563ec7581496c8cac1c77a3be8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 6 Mar 2022 16:45:41 +0000 Subject: [PATCH 106/165] Fix regression with homekit_controller + Aqara motion/vibration sensors (#67740) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index dfd45991b3f..9ca447ad2fe 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.15"], + "requirements": ["aiohomekit==0.7.16"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 973cfb19c59..2ec33b35621 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -184,7 +184,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.15 +aiohomekit==0.7.16 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37b96d4f8ac..070b4e3b10b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.15 +aiohomekit==0.7.16 # homeassistant.components.emulated_hue # homeassistant.components.http From 4aaafb0a991e96fe1e4c8dee29f3ab12c92a3b72 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Mar 2022 15:38:33 +0100 Subject: [PATCH 107/165] Fix false positive MQTT climate deprecation warnings for defaults (#67661) Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/climate.py | 32 +++++-- tests/components/mqtt/test_climate.py | 113 +++++++++++++++++++++++ 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index e145edde7d7..94320cc5def 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -271,7 +271,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list, + vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -298,7 +298,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( ), vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, + vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together @@ -431,6 +431,12 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + self._send_if_off = True + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + self._hold_list = [] + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -499,6 +505,15 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + if CONF_SEND_IF_OFF in config: + self._send_if_off = config[CONF_SEND_IF_OFF] + + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + if CONF_HOLD_LIST in config: + self._hold_list = config[CONF_HOLD_LIST] + def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -806,7 +821,9 @@ class MqttClimate(MqttEntity, ClimateEntity): ): presets.append(PRESET_AWAY) - presets.extend(self._config[CONF_HOLD_LIST]) + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + presets.extend(self._hold_list) if presets: presets.insert(0, PRESET_NONE) @@ -847,10 +864,7 @@ class MqttClimate(MqttEntity, ClimateEntity): setattr(self, attr, temp) # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if ( - self._config[CONF_SEND_IF_OFF] - or self._current_operation != HVAC_MODE_OFF - ): + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) @@ -890,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( swing_mode ) @@ -903,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c3501267e12..93249e76875 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -333,6 +333,43 @@ async def test_set_fan_mode(hass, mqtt_mock): assert state.attributes.get("fan_mode") == "high" +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("fan-mode-topic", "low", 0, False)]), + ({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_fan_mode_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of fan mode if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for fan_mode should be sent when the device is turned on + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -385,6 +422,43 @@ async def test_set_swing(hass, mqtt_mock): assert state.attributes.get("swing_mode") == "on" +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("swing-mode-topic", "on", 0, False)]), + ({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_swing_mode_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of swing mode if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for swing_mode should be sent when the device is turned on + await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_target_temperature(hass, mqtt_mock): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -421,6 +495,45 @@ async def test_set_target_temperature(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("temperature-topic", "21.0", 0, False)]), + ({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_target_temperature_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of target temperature if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for target temperature should be sent when the device is turned on + await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "temperature-topic", "16.0", 0, False + ) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for target temperature sent should be if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_target_temperature_pessimistic(hass, mqtt_mock): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) From 87492e6b3e97f4211e3187ff9f73343f9311d802 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Mon, 7 Mar 2022 09:14:05 +0000 Subject: [PATCH 108/165] Fix timezone for growatt lastdataupdate (#67684) * Added timezone for growatt lastdataupdate (#67646) * Growatt lastdataupdate set to local timezone --- homeassistant/components/growatt_server/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 67095492de7..db045242987 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -222,7 +222,7 @@ class GrowattData: date_now = dt.now().date() last_updated_time = dt.parse_time(str(sorted_keys[-1])) mix_detail["lastdataupdate"] = datetime.datetime.combine( - date_now, last_updated_time + date_now, last_updated_time, dt.DEFAULT_TIME_ZONE ) # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined From 814c96834efd2ce108eea7df696a1e2030ab2a41 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Mar 2022 18:55:12 +0100 Subject: [PATCH 109/165] Fix temperature stepping in Sensibo (#67737) Co-authored-by: Paulus Schoutsen --- homeassistant/components/sensibo/coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index ef0475640b5..a76654e3c68 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -15,6 +15,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT +MAX_POSSIBLE_STEP = 1000 + class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" @@ -74,7 +76,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): .get("values", [0, 1]) ) if temperatures_list: - temperature_step = temperatures_list[1] - temperatures_list[0] + diff = MAX_POSSIBLE_STEP + for i in range(len(temperatures_list) - 1): + if temperatures_list[i + 1] - temperatures_list[i] < diff: + diff = temperatures_list[i + 1] - temperatures_list[i] + temperature_step = diff active_features = list(ac_states) full_features = set() From f4ec7e0902b4eef30e92fa84eb718d09e3d7fa11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 05:42:16 +0100 Subject: [PATCH 110/165] Prevent polling from recreating an entity after removal (#67750) --- homeassistant/helpers/entity.py | 30 +++++++++++++++++++++------ tests/helpers/test_entity.py | 16 ++++++++++++++ tests/helpers/test_entity_platform.py | 24 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8e4f6bc8b58..a554a093c5c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Awaitable, Iterable, Mapping, MutableMapping from dataclasses import dataclass from datetime import datetime, timedelta +from enum import Enum, auto import functools as ft import logging import math @@ -207,6 +208,19 @@ class EntityCategory(StrEnum): SYSTEM = "system" +class EntityPlatformState(Enum): + """The platform state of an entity.""" + + # Not Added: Not yet added to a platform, polling updates are written to the state machine + NOT_ADDED = auto() + + # Added: Added to a platform, polling updates are written to the state machine + ADDED = auto() + + # Removed: Removed from a platform, polling updates are not written to the state machine + REMOVED = auto() + + def convert_to_entity_category( value: EntityCategory | str | None, raise_report: bool = True ) -> EntityCategory | None: @@ -294,7 +308,7 @@ class Entity(ABC): _context_set: datetime | None = None # If entity is added to an entity platform - _added = False + _platform_state = EntityPlatformState.NOT_ADDED # Entity Properties _attr_assumed_state: bool = False @@ -553,6 +567,10 @@ class Entity(ABC): @callback def _async_write_ha_state(self) -> None: """Write the state to the state machine.""" + if self._platform_state == EntityPlatformState.REMOVED: + # Polling returned after the entity has already been removed + return + if self.registry_entry and self.registry_entry.disabled_by: if not self._disabled_reported: self._disabled_reported = True @@ -758,7 +776,7 @@ class Entity(ABC): parallel_updates: asyncio.Semaphore | None, ) -> None: """Start adding an entity to a platform.""" - if self._added: + if self._platform_state == EntityPlatformState.ADDED: raise HomeAssistantError( f"Entity {self.entity_id} cannot be added a second time to an entity platform" ) @@ -766,7 +784,7 @@ class Entity(ABC): self.hass = hass self.platform = platform self.parallel_updates = parallel_updates - self._added = True + self._platform_state = EntityPlatformState.ADDED @callback def add_to_platform_abort(self) -> None: @@ -774,7 +792,7 @@ class Entity(ABC): self.hass = None # type: ignore[assignment] self.platform = None self.parallel_updates = None - self._added = False + self._platform_state = EntityPlatformState.NOT_ADDED async def add_to_platform_finish(self) -> None: """Finish adding an entity to a platform.""" @@ -792,12 +810,12 @@ class Entity(ABC): If the entity doesn't have a non disabled entry in the entity registry, or if force_remove=True, its state will be removed. """ - if self.platform and not self._added: + if self.platform and self._platform_state != EntityPlatformState.ADDED: raise HomeAssistantError( f"Entity {self.entity_id} async_remove called twice" ) - self._added = False + self._platform_state = EntityPlatformState.REMOVED if self._on_remove is not None: while self._on_remove: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 6b7de074a24..afc0887371e 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -545,6 +545,22 @@ async def test_async_remove_runs_callbacks(hass): assert len(result) == 1 +async def test_async_remove_ignores_in_flight_polling(hass): + """Test in flight polling is ignored after removing.""" + result = [] + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "test.test" + ent.async_on_remove(lambda: result.append(1)) + ent.async_write_ha_state() + assert hass.states.get("test.test").state == STATE_UNKNOWN + await ent.async_remove() + assert len(result) == 1 + assert hass.states.get("test.test") is None + ent.async_write_ha_state() + + async def test_set_context(hass): """Test setting context.""" context = Context() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9aa0a849e5a..c98fdff7858 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -390,6 +390,30 @@ async def test_async_remove_with_platform(hass): assert len(hass.states.async_entity_ids()) == 0 +async def test_async_remove_with_platform_update_finishes(hass): + """Remove an entity when an update finishes after its been removed.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name="test_1") + + async def _delayed_update(*args, **kwargs): + await asyncio.sleep(0.01) + + entity1.async_update = _delayed_update + + # Add, remove, add, remove and make sure no updates + # cause the entity to reappear after removal + for i in range(2): + await component.async_add_entities([entity1]) + assert len(hass.states.async_entity_ids()) == 1 + entity1.async_write_ha_state() + assert hass.states.get(entity1.entity_id) is not None + task = asyncio.create_task(entity1.async_update_ha_state(True)) + await entity1.async_remove() + assert len(hass.states.async_entity_ids()) == 0 + await task + assert len(hass.states.async_entity_ids()) == 0 + + async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): """Test for not adding duplicate entities.""" caplog.set_level(logging.ERROR) From c807c57a9bf3693fe0d1e72368b86e4253e87888 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Mar 2022 17:23:08 +0100 Subject: [PATCH 111/165] Fix internet access switch for old discovery (#67777) --- 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 4c307c126cd..21039d45afa 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): ) self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): + if info.ip_address: + info.wan_access = self._get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True self.send_signal_device_update(new_device) From dfa1c3abb351e17565120522da385d56bbf8a7ae Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Mar 2022 18:05:10 +0100 Subject: [PATCH 112/165] Fix profile name update for Shelly Valve (#67778) --- homeassistant/components/shelly/climate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 2c81ecbe183..1e7ba2dd183 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -317,4 +317,14 @@ class BlockSleepingClimate( if self.device_block and self.block: _LOGGER.debug("Entity %s attached to blocks", self.name) + + assert self.block.channel + + self._preset_modes = [ + PRESET_NONE, + *self.wrapper.device.settings["thermostats"][int(self.block.channel)][ + "schedule_profile_names" + ], + ] + self.async_write_ha_state() From 8d7cdceb75482e2b6a68a20d714073e92acb6ea0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Mar 2022 12:10:38 -0500 Subject: [PATCH 113/165] Handle fan_modes being set to None in homekit (#67790) --- .../components/homekit/type_thermostats.py | 25 ++-- .../homekit/test_type_thermostats.py | 127 ++++++++++++++++++ 2 files changed, 139 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 8c54896e85e..1e20d1bc710 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -285,20 +285,19 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_HUMIDITY, value=50 ) - fan_modes = self.fan_modes = { - fan_mode.lower(): fan_mode - for fan_mode in attributes.get(ATTR_FAN_MODES, []) - } + fan_modes = {} self.ordered_fan_speeds = [] - if ( - features & SUPPORT_FAN_MODE - and fan_modes - and PRE_DEFINED_FAN_MODES.intersection(fan_modes) - ): - self.ordered_fan_speeds = [ - speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes - ] - self.fan_chars.append(CHAR_ROTATION_SPEED) + + if features & SUPPORT_FAN_MODE: + fan_modes = { + fan_mode.lower(): fan_mode + for fan_mode in attributes.get(ATTR_FAN_MODES) or [] + } + if fan_modes and PRE_DEFINED_FAN_MODES.intersection(fan_modes): + self.ordered_fan_speeds = [ + speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes + ] + self.fan_chars.append(CHAR_ROTATION_SPEED) if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds): self.fan_chars.append(CHAR_TARGET_FAN_STATE) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d1db618e7e4..5f002fbbf6c 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -15,6 +15,8 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, @@ -74,6 +76,7 @@ from homeassistant.components.homekit.type_thermostats import ( from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, @@ -2349,3 +2352,127 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): assert len(call_set_fan_mode) == 2 assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF + + +async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events): + """Test a thermostate with fan modes set to None.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: None, + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + + +async def test_thermostat_with_fan_modes_set_to_none_not_supported( + hass, hk_driver, events +): + """Test a thermostate with fan modes set to None and supported feature missing.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: None, + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + + +async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( + hass, hk_driver, events +): + """Test a thermostate with fan mode and supported feature missing.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, + ATTR_MIN_TEMP: 44.6, + ATTR_MAX_TEMP: 95, + ATTR_PRESET_MODES: ["home", "away"], + ATTR_TEMPERATURE: 67, + ATTR_TARGET_TEMP_HIGH: None, + ATTR_TARGET_TEMP_LOW: None, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_FAN_MODES: None, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_PRESET_MODE: "home", + ATTR_FRIENDLY_NAME: "Rec Room", + ATTR_HVAC_MODES: [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.ordered_fan_speeds == [] + assert not acc.fan_chars From 97ba17d1eca406cd06dfe35d72d437e63e9fa2f2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Mar 2022 18:14:14 +0100 Subject: [PATCH 114/165] Catch Elgato connection errors (#67799) --- homeassistant/components/elgato/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 805a70613f9..f15ccc0a03d 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -1,13 +1,13 @@ """Support for Elgato Lights.""" from typing import NamedTuple -from elgato import Elgato, Info, State +from elgato import Elgato, ElgatoConnectionError, Info, State from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant 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 DOMAIN, LOGGER, SCAN_INTERVAL @@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) + async def _async_update_data() -> State: + """Fetch Elgato data.""" + try: + return await elgato.state() + except ElgatoConnectionError as err: + raise UpdateFailed(err) from err + coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator( hass, LOGGER, name=f"{DOMAIN}_{entry.data[CONF_HOST]}", update_interval=SCAN_INTERVAL, - update_method=elgato.state, + update_method=_async_update_data, ) await coordinator.async_config_entry_first_refresh() From 580c998552b3e9578c2db418b923ae63c9315df7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 17:56:52 +0100 Subject: [PATCH 115/165] Update frontend to 20220301.1 (#67812) --- homeassistant/components/frontend/manifest.json | 5 +++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cc118e23dc9..baf61343040 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==20220301.0" + "home-assistant-frontend==20220301.1" ], "dependencies": [ "api", @@ -13,7 +13,8 @@ "diagnostics", "http", "lovelace", - "onboarding", "search", + "onboarding", + "search", "system_log", "websocket_api" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bd9e1da6a69..2a441dffc35 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2ec33b35621..53849ece349 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -843,7 +843,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 070b4e3b10b..d0f8c9513a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 4e6fc3615b16568caeb1b47bb387b6526a9fbfc7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 8 Mar 2022 00:43:05 +0100 Subject: [PATCH 116/165] Bump python-miio version to 0.5.11 (#67824) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 0091d58e1e2..7157e32299a 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.10"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 53849ece349..e74e0915572 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1952,7 +1952,7 @@ python-kasa==0.4.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.10 +python-miio==0.5.11 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0f8c9513a0..2031915fc1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1219,7 +1219,7 @@ python-juicenet==1.0.2 python-kasa==0.4.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.10 +python-miio==0.5.11 # homeassistant.components.nest python-nest==4.2.0 From b09ab2dafbfb3a78d7214b6a39c56fcf3856d63e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 05:43:19 +0100 Subject: [PATCH 117/165] Prevent scene from restoring unavailable states (#67836) --- homeassistant/components/scene/__init__.py | 8 ++++-- tests/components/scene/test_init.py | 29 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 846c0fbc7c6..5dea5965d43 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ATTR_TRANSITION from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON +from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -117,7 +117,11 @@ class Scene(RestoreEntity): """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: + if ( + state is not None + and state.state is not None + and state.state != STATE_UNAVAILABLE + ): self.__last_activated = state.state def activate(self, **kwargs: Any) -> None: diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 41b16261cd1..3dd0cfce7b9 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State @@ -177,6 +178,34 @@ async def test_restore_state(hass, entities, enable_custom_integrations): assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00" +async def test_restore_state_does_not_restore_unavailable( + hass, entities, enable_custom_integrations +): + """Test we restore state integration but ignore unavailable.""" + mock_restore_cache(hass, (State("scene.test", STATE_UNAVAILABLE),)) + + light_1, light_2 = await setup_lights(hass, entities) + + assert await async_setup_component( + hass, + scene.DOMAIN, + { + "scene": [ + { + "name": "test", + "entities": { + light_1.entity_id: "on", + light_2.entity_id: "on", + }, + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("scene.test").state == STATE_UNKNOWN + + async def activate(hass, entity_id=ENTITY_MATCH_ALL): """Activate a scene.""" data = {} From a1abcbc7ebf386dc52c3c2292b74eb38e0dfad38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2022 20:45:49 -0800 Subject: [PATCH 118/165] Bumped version to 2022.3.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 e91d5cf3725..c1d791aaf07 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 = 3 -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 e9a6dbb8ee5..a3c8068edb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.2 +version = 2022.3.3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 28b3edf6b2a414ebc68b1944586dfd56c8eef26a Mon Sep 17 00:00:00 2001 From: cheng2wei <99464193+cheng2wei@users.noreply.github.com> Date: Tue, 8 Mar 2022 20:01:00 -0800 Subject: [PATCH 119/165] Fix discord embed class initialization (#67831) --- homeassistant/components/discord/notify.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 41137e1a32c..2e13b68225c 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -20,9 +20,13 @@ _LOGGER = logging.getLogger(__name__) ATTR_EMBED = "embed" ATTR_EMBED_AUTHOR = "author" +ATTR_EMBED_COLOR = "color" +ATTR_EMBED_DESCRIPTION = "description" ATTR_EMBED_FIELDS = "fields" ATTR_EMBED_FOOTER = "footer" +ATTR_EMBED_TITLE = "title" ATTR_EMBED_THUMBNAIL = "thumbnail" +ATTR_EMBED_URL = "url" ATTR_IMAGES = "images" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) @@ -64,10 +68,16 @@ class DiscordNotificationService(BaseNotificationService): embeds: list[nextcord.Embed] = [] if ATTR_EMBED in data: embedding = data[ATTR_EMBED] + title = embedding.get(ATTR_EMBED_TITLE) or nextcord.Embed.Empty + description = embedding.get(ATTR_EMBED_DESCRIPTION) or nextcord.Embed.Empty + color = embedding.get(ATTR_EMBED_COLOR) or nextcord.Embed.Empty + url = embedding.get(ATTR_EMBED_URL) or nextcord.Embed.Empty fields = embedding.get(ATTR_EMBED_FIELDS) or [] if embedding: - embed = nextcord.Embed(**embedding) + embed = nextcord.Embed( + title=title, description=description, color=color, url=url + ) for field in fields: embed.add_field(**field) if ATTR_EMBED_FOOTER in embedding: From f5b6d93706dcc621be38471cafe181a76c467149 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Tue, 8 Mar 2022 16:56:15 +0100 Subject: [PATCH 120/165] Support playing local "file" media on Kodi (#67832) Co-authored-by: Paulus Schoutsen --- homeassistant/components/kodi/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 53798a7ccd9..e1dae879bf8 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -717,6 +717,8 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.play_channel(int(media_id)) elif media_type_lower == MEDIA_TYPE_PLAYLIST: await self._kodi.play_playlist(int(media_id)) + elif media_type_lower == "file": + await self._kodi.play_file(media_id) elif media_type_lower == "directory": await self._kodi.play_directory(media_id) elif media_type_lower in [ From c22af2c82a002f1f26ba15a14107ed87789609ff Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 8 Mar 2022 03:44:39 -0500 Subject: [PATCH 121/165] Bump zwave-js-server-python to 0.35.2 (#67839) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 6a892b2791d..8d1a9091bc0 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.35.1"], + "requirements": ["zwave-js-server-python==0.35.2"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index e74e0915572..1a22f6f89d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2566,7 +2566,7 @@ zigpy==0.43.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.35.1 +zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me zwave_me_ws==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2031915fc1d..d802fe91bc0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1588,7 +1588,7 @@ zigpy-znp==0.7.0 zigpy==0.43.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.35.1 +zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me zwave_me_ws==0.2.1 From cad397d6a7d7221c18fbeb2695683f99e98e453d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 10:16:41 +0100 Subject: [PATCH 122/165] Add missing callback decorator to sun (#67840) --- homeassistant/components/sun/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index fd819e5ad33..4789490ef0d 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -101,6 +101,7 @@ class Sun(Entity): self.rising = self.phase = None self._next_change = None + @callback def update_location(_event): location, elevation = get_astral_location(self.hass) if location == self.location: From 07e70c81b0a41a1c856522fdf51ccb462d77f4a3 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 8 Mar 2022 19:56:57 +0200 Subject: [PATCH 123/165] Fix shelly duo scene restore (#67871) --- homeassistant/components/shelly/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index b0fb9cd1a8a..13a7720e7ed 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -336,7 +336,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): ATTR_RGBW_COLOR ] - if ATTR_EFFECT in kwargs: + if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs: # Color effect change - used only in color mode, switch device mode to color set_mode = "color" if self.wrapper.model == "SHBLB-1": From c608cafebdc28f0bb49aef83c9f8d3152964d579 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 9 Mar 2022 20:21:31 +0100 Subject: [PATCH 124/165] Make sure blueprint cache is flushed on script reload (#67899) --- homeassistant/components/script/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 593e585c4a5..2b9c9976ce4 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -175,7 +175,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Call a service to reload scripts.""" if (conf := await component.async_prepare_reload()) is None: return - + async_get_blueprints(hass).async_reset_cache() await _async_process_config(hass, conf, component) async def turn_on_service(service: ServiceCall) -> None: From 5a39e63d2548c707dff0163ce6b86ce9a953ab56 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Mar 2022 20:22:10 +0100 Subject: [PATCH 125/165] Update radios to 0.1.1 (#67902) --- homeassistant/components/radio_browser/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json index 865d8b25ab1..9c6858ae27e 100644 --- a/homeassistant/components/radio_browser/manifest.json +++ b/homeassistant/components/radio_browser/manifest.json @@ -3,7 +3,7 @@ "name": "Radio Browser", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/radio", - "requirements": ["radios==0.1.0"], + "requirements": ["radios==0.1.1"], "codeowners": ["@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 1a22f6f89d0..0538b9332dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2079,7 +2079,7 @@ quantum-gateway==0.0.6 rachiopy==1.0.3 # homeassistant.components.radio_browser -radios==0.1.0 +radios==0.1.1 # homeassistant.components.radiotherm radiotherm==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d802fe91bc0..d08c7b1740e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1295,7 +1295,7 @@ pyzerproc==0.4.8 rachiopy==1.0.3 # homeassistant.components.radio_browser -radios==0.1.0 +radios==0.1.1 # homeassistant.components.rainmachine regenmaschine==2022.01.0 From ca664ab5a5d431e9cb3002c7b5419cf5286c69fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 10:32:49 +0100 Subject: [PATCH 126/165] Correct local import of paho-mqtt (#67944) * Correct local import of paho-mqtt * Remove MqttClientSetup.mqtt class attribute * Remove reference to MqttClientSetup.mqtt --- homeassistant/components/mqtt/__init__.py | 23 ++++++++++++-------- homeassistant/components/mqtt/config_flow.py | 6 ++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c6229c2d475..53a2544611a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -13,7 +13,7 @@ import logging from operator import attrgetter import ssl import time -from typing import Any, Union, cast +from typing import TYPE_CHECKING, Any, Union, cast import uuid import attr @@ -113,6 +113,11 @@ from .models import ( ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +if TYPE_CHECKING: + # Only import for paho-mqtt type checking here, imports are done locally + # because integrations should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -759,23 +764,23 @@ class Subscription: class MqttClientSetup: """Helper class to setup the paho mqtt client from config.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - def __init__(self, config: ConfigType) -> None: """Initialize the MQTT client setup helper.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + if config[CONF_PROTOCOL] == PROTOCOL_31: - proto = self.mqtt.MQTTv31 + proto = mqtt.MQTTv31 else: - proto = self.mqtt.MQTTv311 + proto = mqtt.MQTTv311 if (client_id := config.get(CONF_CLIENT_ID)) is None: # PAHO MQTT relies on the MQTT server to generate random client IDs. # However, that feature is not mandatory so we generate our own. - client_id = self.mqtt.base62(uuid.uuid4().int, padding=22) - self._client = self.mqtt.Client(client_id, protocol=proto) + client_id = mqtt.base62(uuid.uuid4().int, padding=22) + self._client = mqtt.Client(client_id, protocol=proto) # Enable logging self._client.enable_logger() diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 99e7e9718d0..6697b17dfdc 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -319,6 +319,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): def try_connection(hass, broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + # Get the config from configuration.yaml yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) entry_config = { @@ -334,7 +338,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"): def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" - result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED) + result.put(result_code == mqtt.CONNACK_ACCEPTED) client.on_connect = on_connect From 1bfb01e0d1a5c18f01c51d350d80938eeb1a8e07 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 10 Mar 2022 10:50:36 -0500 Subject: [PATCH 127/165] Rollback pyinsteon (#67956) --- 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 7abff39113b..b069f3b18a8 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.16" + "pyinsteon==1.0.13" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index 0538b9332dc..0dac21d8cbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1589,7 +1589,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.0.16 +pyinsteon==1.0.13 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d08c7b1740e..29a9b6e8088 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1000,7 +1000,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.0.16 +pyinsteon==1.0.13 # homeassistant.components.ipma pyipma==2.0.5 From 7de5e070fbca19ab932b46b8934268b5fd18463d Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 11 Mar 2022 18:24:08 +0100 Subject: [PATCH 128/165] Bump pysabnzbd to 1.1.1 (#67971) --- homeassistant/components/sabnzbd/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 08fb1388b38..f6cbd958206 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -2,7 +2,7 @@ "domain": "sabnzbd", "name": "SABnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd", - "requirements": ["pysabnzbd==1.1.0"], + "requirements": ["pysabnzbd==1.1.1"], "dependencies": ["configurator"], "after_dependencies": ["discovery"], "codeowners": [], diff --git a/requirements_all.txt b/requirements_all.txt index 0dac21d8cbf..195572800c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,7 @@ pyrituals==0.0.6 pyruckus==0.12 # homeassistant.components.sabnzbd -pysabnzbd==1.1.0 +pysabnzbd==1.1.1 # homeassistant.components.saj pysaj==0.0.16 From 7fb76c68bb5a146ff5d4e7689acb455b2f43556a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Mar 2022 09:25:55 -0800 Subject: [PATCH 129/165] Bumped version to 2022.3.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 c1d791aaf07..9f3d5e1c0f7 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 = 3 -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 a3c8068edb1..5cdcab49caf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.3 +version = 2022.3.4 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 3de341099fc8c586e8bef8ba1d2b432a99a66853 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 11 Mar 2022 23:45:29 +0000 Subject: [PATCH 130/165] Bump pymediaroom (#68016) --- homeassistant/components/mediaroom/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 63007f88bbb..c49368d2e27 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -2,7 +2,7 @@ "domain": "mediaroom", "name": "Mediaroom", "documentation": "https://www.home-assistant.io/integrations/mediaroom", - "requirements": ["pymediaroom==0.6.4.1"], + "requirements": ["pymediaroom==0.6.5.4"], "codeowners": ["@dgomes"], "iot_class": "local_polling", "loggers": ["pymediaroom"] diff --git a/requirements_all.txt b/requirements_all.txt index 195572800c2..8246fd24dbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1670,7 +1670,7 @@ pymata-express==1.19 pymazda==0.3.2 # homeassistant.components.mediaroom -pymediaroom==0.6.4.1 +pymediaroom==0.6.5.4 # homeassistant.components.melcloud pymelcloud==2.5.6 From c0860931b3848168170cc79eea8ba7bbe643f340 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 14 Mar 2022 17:32:14 +0100 Subject: [PATCH 131/165] Fix WebSocketTimeoutException in SamsungTV (#68114) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 74daf1d34e0..481d3588bb7 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -10,7 +10,7 @@ from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse from samsungtvws import SamsungTVWS from samsungtvws.exceptions import ConnectionFailure, HttpApiError -from websocket import WebSocketException +from websocket import WebSocketException, WebSocketTimeoutException from homeassistant.const import ( CONF_HOST, @@ -318,8 +318,8 @@ class SamsungTVWSBridge(SamsungTVBridge): def _get_app_list(self) -> dict[str, str] | None: """Get installed app list.""" - if self._app_list is None: - if remote := self._get_remote(): + if self._app_list is None and (remote := self._get_remote()): + with contextlib.suppress(WebSocketTimeoutException): raw_app_list: list[dict[str, str]] = remote.app_list() self._app_list = { app["name"]: app["appId"] From c95d55e6d638ae1f9dbf334b01628c01fb2eb787 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 14 Mar 2022 12:07:58 -0500 Subject: [PATCH 132/165] 20220301.2 (#68130) --- 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 baf61343040..bcbf5b81ff8 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==20220301.1" + "home-assistant-frontend==20220301.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2a441dffc35..20b3c5badd3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220301.1 +home-assistant-frontend==20220301.2 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 8246fd24dbb..503be5656cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -843,7 +843,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.1 +home-assistant-frontend==20220301.2 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29a9b6e8088..edf4eb3a12f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.1 +home-assistant-frontend==20220301.2 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From a285478cf8e3e82508df936c3af36ec19f71bba4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Mar 2022 02:09:02 -1000 Subject: [PATCH 133/165] Filter IPv6 addresses from doorbird discovery (#68031) --- .../components/doorbird/config_flow.py | 4 +- .../components/doorbird/translations/en.json | 3 +- tests/components/doorbird/test_config_flow.py | 48 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 31ddd1f6193..cc882b0ed50 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.util.network import is_link_local +from homeassistant.util.network import is_ipv4_address, is_link_local from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI from .util import get_mac_address_from_doorstation_info @@ -103,6 +103,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") + if not is_ipv4_address(host): + return self.async_abort(reason="not_ipv4_address") await self.async_set_unique_id(macaddress) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index db1cea2d73f..c67658196c4 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Device is already configured", "link_local_address": "Link local addresses are not supported", - "not_doorbird_device": "This device is not a DoorBird" + "not_doorbird_device": "This device is not a DoorBird", + "not_ipv4_address": "Only IPv4 addresess are supported" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 10cc4a4fb77..cbf455653ff 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -116,6 +116,54 @@ async def test_form_zeroconf_link_local_ignored(hass): assert result["reason"] == "link_local_address" +async def test_form_zeroconf_ipv4_address(hass): + """Test we abort and update the ip address from zeroconf with an ipv4 address.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1CCAE3AAAAAA", + data=VALID_CONFIG, + options={CONF_EVENTS: ["event1", "event2", "event3"]}, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="4.4.4.4", + addresses=["4.4.4.4"], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3AAAAAA"}, + type="mock_type", + ), + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == "4.4.4.4" + + +async def test_form_zeroconf_non_ipv4_ignored(hass): + """Test we abort when we get a non ipv4 address via zeroconf.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3DOORBIRD"}, + type="mock_type", + ), + ) + assert result["type"] == "abort" + assert result["reason"] == "not_ipv4_address" + + async def test_form_zeroconf_correct_oui(hass): """Test we can setup from zeroconf with the correct OUI source.""" doorbirdapi = _get_mock_doorbirdapi_return_values( From a42ba9e10a0bbd0a875a2202f7a4eb7d24ee9a0a Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Sat, 12 Mar 2022 13:42:28 -0500 Subject: [PATCH 134/165] Fix turning amcrest camera on and off (#68050) --- homeassistant/components/amcrest/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 3846f4945a9..49d5cfd7afd 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -548,7 +548,7 @@ class AmcrestCam(Camera): # recording on if video stream is being turned off. if self.is_recording and not enable: await self._async_enable_recording(False) - await self._async_change_setting(enable, "video", "is_streaming") + await self._async_change_setting(enable, "video", "_attr_is_streaming") if self._control_light: await self._async_change_light() From 31b19e09b58cf2756fc43511d363c15e04d27e60 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 12 Mar 2022 22:21:45 +0200 Subject: [PATCH 135/165] Fix Shelly EM/3EM invalid energy value after reboot (#68052) --- homeassistant/components/shelly/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 21a7447e2b2..7e19b9724d7 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -174,6 +174,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + available=lambda block: cast(int, block.energy) != -1, ), ("emeter", "energyReturned"): BlockSensorDescription( key="emeter|energyReturned", @@ -182,6 +183,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + available=lambda block: cast(int, block.energyReturned) != -1, ), ("light", "energy"): BlockSensorDescription( key="light|energy", From 6d8bd6af4d1b3f11881cb2e345799983e1b0f8aa Mon Sep 17 00:00:00 2001 From: Christopher Thornton Date: Sat, 12 Mar 2022 23:46:35 -0800 Subject: [PATCH 136/165] Default somfy_mylink shade's _attr_is_closed to `None` (#68053) The base `Cover` entity requires an explicit value for `_attr_is_closed`. Since the `SomfyShade` is an assumed state, we don't know by default whether the shade is open or not, so we need to explicitly return `None` for `_attr_is_closed` --- homeassistant/components/somfy_mylink/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index 7ac0aace47f..d1b09175deb 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -79,6 +79,7 @@ class SomfyShade(RestoreEntity, CoverEntity): self._attr_unique_id = target_id self._attr_name = name self._reverse = reverse + self._attr_is_closed = None self._attr_device_class = device_class self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._target_id)}, From 69587dd50a89412c9a8ae13e66aade30fc675056 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Mon, 14 Mar 2022 12:36:16 -0400 Subject: [PATCH 137/165] Bump amcrest version to 1.9.7 (#68055) --- homeassistant/components/amcrest/__init__.py | 3 +-- homeassistant/components/amcrest/camera.py | 16 +++++++++++----- homeassistant/components/amcrest/const.py | 3 +++ homeassistant/components/amcrest/manifest.json | 2 +- requirements_all.txt | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 166d2e74ffd..f2472575259 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -52,6 +52,7 @@ from .const import ( DATA_AMCREST, DEVICES, DOMAIN, + RESOLUTION_LIST, SERVICE_EVENT, SERVICE_UPDATE, ) @@ -76,8 +77,6 @@ RECHECK_INTERVAL = timedelta(minutes=1) NOTIFICATION_ID = "amcrest_notification" NOTIFICATION_TITLE = "Amcrest Camera Setup" -RESOLUTION_LIST = {"high": 0, "low": 1} - SCAN_INTERVAL = timedelta(seconds=10) AUTHENTICATION_LIST = {"basic": "basic"} diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 49d5cfd7afd..61b97b041cd 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -35,6 +35,7 @@ from .const import ( DATA_AMCREST, DEVICES, DOMAIN, + RESOLUTION_TO_STREAM, SERVICE_UPDATE, SNAPSHOT_TIMEOUT, ) @@ -533,13 +534,14 @@ class AmcrestCam(Camera): return async def _async_get_video(self) -> bool: - stream = {0: "Main", 1: "Extra"} return await self._api.async_is_video_enabled( - channel=0, stream=stream[self._resolution] + channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] ) async def _async_set_video(self, enable: bool) -> None: - await self._api.async_set_video_enabled(enable, channel=0) + await self._api.async_set_video_enabled( + enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] + ) async def _async_enable_video(self, enable: bool) -> None: """Enable or disable camera video stream.""" @@ -585,10 +587,14 @@ class AmcrestCam(Camera): ) async def _async_get_audio(self) -> bool: - return await self._api.async_audio_enabled + return await self._api.async_is_audio_enabled( + channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] + ) async def _async_set_audio(self, enable: bool) -> None: - await self._api.async_set_audio_enabled(enable) + await self._api.async_set_audio_enabled( + enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] + ) async def _async_enable_audio(self, enable: bool) -> None: """Enable or disable audio stream.""" diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 89cde63a08a..6c2fe431d43 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -13,3 +13,6 @@ SNAPSHOT_TIMEOUT = 20 SERVICE_EVENT = "event" SERVICE_UPDATE = "update" + +RESOLUTION_LIST = {"high": 0, "low": 1} +RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"} diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 5a7bec89e31..b4646be5e66 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.4"], + "requirements": ["amcrest==1.9.7"], "dependencies": ["ffmpeg"], "codeowners": ["@flacjacket"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 503be5656cb..22d58dc484f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -311,7 +311,7 @@ amberelectric==1.0.3 ambiclimate==0.2.1 # homeassistant.components.amcrest -amcrest==1.9.4 +amcrest==1.9.7 # homeassistant.components.androidtv androidtv[async]==0.0.63 From 7aecd69e3b1cd08da89886e7fa4ddd2becd99ded Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Mar 2022 04:33:55 -1000 Subject: [PATCH 138/165] Bump pyisy to 3.0.5 (#68069) * Bump pyisy to 3.0.4 - Fixes #66003 - Changelog: https://github.com/automicus/PyISY/compare/v3.0.1...v3.0.4 * again --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index fe0fa9720ca..89e66533913 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.1"], + "requirements": ["pyisy==3.0.5"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 22d58dc484f..d16602830df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1610,7 +1610,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.1 +pyisy==3.0.5 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edf4eb3a12f..7e468fcf68f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1015,7 +1015,7 @@ pyiqvia==2021.11.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.1 +pyisy==3.0.5 # homeassistant.components.kira pykira==0.1.1 From 479a230da715387071232cd3abed4cb0fa280424 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Sun, 13 Mar 2022 15:31:57 +0100 Subject: [PATCH 139/165] Update home_connect to 0.7.0 (#68089) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 5667d539902..784042f884c 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "dependencies": ["http"], "codeowners": ["@DavidMStraub"], - "requirements": ["homeconnect==0.6.3"], + "requirements": ["homeconnect==0.7.0"], "config_flow": true, "iot_class": "cloud_push", "loggers": ["homeconnect"] diff --git a/requirements_all.txt b/requirements_all.txt index d16602830df..565cdcae1a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -849,7 +849,7 @@ home-assistant-frontend==20220301.2 # homeassistant-pyozw==0.1.10 # homeassistant.components.home_connect -homeconnect==0.6.3 +homeconnect==0.7.0 # homeassistant.components.homematicip_cloud homematicip==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e468fcf68f..0ec2c864424 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ home-assistant-frontend==20220301.2 # homeassistant-pyozw==0.1.10 # homeassistant.components.home_connect -homeconnect==0.6.3 +homeconnect==0.7.0 # homeassistant.components.homematicip_cloud homematicip==1.0.2 From b86d115764f78640792374ceae401b4a7093c45b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 14 Mar 2022 12:39:44 +0100 Subject: [PATCH 140/165] Fix MQTT false positive deprecation warnings (#68117) --- homeassistant/components/mqtt/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 53a2544611a..1bb318f11c5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -135,6 +135,13 @@ DEFAULT_KEEPALIVE = 60 DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_TLS_PROTOCOL = "auto" +DEFAULT_VALUES = { + CONF_PORT: DEFAULT_PORT, + CONF_WILL_MESSAGE: DEFAULT_WILL, + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, +} + ATTR_TOPIC_TEMPLATE = "topic_template" ATTR_PAYLOAD_TEMPLATE = "payload_template" @@ -190,7 +197,7 @@ CONFIG_SCHEMA_BASE = vol.Schema( vol.Coerce(int), vol.Range(min=15) ), vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), @@ -207,9 +214,9 @@ CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) ), - vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. vol.Optional( @@ -613,6 +620,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _merge_config(entry, conf): """Merge configuration.yaml config with config entry.""" + # Base config on default values + conf = {**DEFAULT_VALUES, **conf} return {**conf, **entry.data} @@ -632,6 +641,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: override, ) + # Merge the configuration values from configuration.yaml conf = _merge_config(entry, conf) hass.data[DATA_MQTT] = MQTT( From 1191c095f890d2927396c271141ad1f199d889b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Mar 2022 22:46:08 -0700 Subject: [PATCH 141/165] Bumped version to 2022.3.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 9f3d5e1c0f7..36f5c21b279 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 = 3 -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 5cdcab49caf..5e229a0da9d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.4 +version = 2022.3.5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 27d275e6f7c2ff0506904c2e5dd67f8041d8f5de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 15:32:56 +0100 Subject: [PATCH 142/165] Fix Efergy tests (#68086) --- tests/components/efergy/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/components/efergy/__init__.py b/tests/components/efergy/__init__.py index ddddc56f4e4..5d77acc6838 100644 --- a/tests/components/efergy/__init__.py +++ b/tests/components/efergy/__init__.py @@ -1,12 +1,11 @@ """Tests for Efergy integration.""" from unittest.mock import AsyncMock, patch -from pyefergy import Efergy, exceptions +from pyefergy import exceptions from homeassistant.components.efergy import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @@ -56,10 +55,6 @@ async def mock_responses( ): """Mock responses from Efergy.""" base_url = "https://engage.efergy.com/mobile_proxy/" - api = Efergy( - token, session=async_get_clientsession(hass), utc_offset="America/New_York" - ) - assert api._utc_offset == 300 if error: aioclient_mock.get( f"{base_url}getInstant?token={token}", From f7fd781a27209d8d0a1c3bc9f32b39b25802b3f0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 18 Mar 2022 09:26:19 +0100 Subject: [PATCH 143/165] Fix TypeError in SamsungTV (#68235) Co-authored-by: epenet --- 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 481d3588bb7..616820aec26 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -319,8 +319,9 @@ class SamsungTVWSBridge(SamsungTVBridge): def _get_app_list(self) -> dict[str, str] | None: """Get installed app list.""" if self._app_list is None and (remote := self._get_remote()): - with contextlib.suppress(WebSocketTimeoutException): + with contextlib.suppress(TypeError, WebSocketTimeoutException): raw_app_list: list[dict[str, str]] = remote.app_list() + LOGGER.debug("Received app list: %s", raw_app_list) self._app_list = { app["name"]: app["appId"] for app in sorted(raw_app_list, key=lambda app: app["name"]) From 9352ed1286f7c9fa05f6971256399176526d489e Mon Sep 17 00:00:00 2001 From: Numa Perez <41305393+nprez83@users.noreply.github.com> Date: Tue, 15 Mar 2022 05:59:18 -0400 Subject: [PATCH 144/165] Fix lyric climate (#67018) * Fixed the issues related to auto mode I was having the same issues as described in #63403, specifically, the error stating that Mode 7 is not valid, only Heat, Cool, Off when trying to do anything while the thermostat is set to Auto. This error originates with the way the Lyric API handles the modes. Basically, when one queries the changeableValues dict, you get a mode=Auto, as well as a heatCoolMode, which is set to either Heat, Cool, Off. Per the documentation, heatCoolMode contains the "heat cool mode when system switch is in Auto mode". It would make sense that when changing the thermostat settings, mode=Auto should be valid, but it's not. The way the API understands that the mode should be set to Auto when changing the thermostat settings is by setting the autoChangeoverActive variable to true, not the mode itself. This require changes in the async_set_hvac_mode, async_set_temperature, and async_set_preset_mode functions. Related to this issue, I got rid of the references to hasDualSetpointStatus, as it seems that it always remains false in the API, even when the mode is set to auto, so again, the key variable for this is autoChangeoverActive. While I was working on this I also noticed another issue. The support flag SUPPORT_TARGET_TEMPERATURE_RANGE had not been included, which did not allow for the temperature range to be available, thus invalidating the target_temperature_low and target_temperature_high functions. I added this flag and sorted out which set point (heat vs cool) should be called for each of them so things work as expected in Lovelace. I have tested all of these functionalities and they all work great on my end, so I thought I'd share. * Update climate.py * Update climate.py Fixed two additional issues: 1) When the system is turned off from Auto, the heatCoolMode variable becomes 'Off', so when you try to restart the system back to Auto, nothing happens. 2) I now prevent the async_set_temperature function from being called with a new set point when the system is Off. All changes tested and functional. * Update climate.py * Update climate.py Return SUPPORT_PRESET_MODE flag only for LCC models (i.e. they have the "thermostatSetpointStatus" variable defined). TCC models do not support this feature * Update climate.py After playing with the official Honeywell API, I realized it doesn't like to received commands with missing data, i.e., it always wants to get a mode, coolSetpoint, heatSetpoint, and autoChangeoverActive variables. This was causing some random issues with changing modes, especially from coming from off, so I modified the async_set_temperature, and async_set_hvac_mode fuctions to always send all pertinent variables. * Update climate.py * Update climate.py * Update climate.py * Update climate.py * Clean code and test everything Alright, sorry for the multiple commits, fixing this properly took a fair bit of testing. I went ahead and cleaned up the code and made the following big picture changes: 1) The integration now supports the Auto mode appropriately, to include the temperature range. 2) There's a bug that actually manifests when using the native app. When the system is 'Off' and you try to turn it on to 'Auto', it will turn on briefly but will go back to 'Off' after a few seconds. When checking the web api, this appears to be related to the fact that the heatCoolMode variable seems to continue to store 'Off', even if the mode accurately displays 'Auto', and the autoChangeoverActive=True. So to overcome that inherent limitation, when the system is 'Off' and the user turns it to 'Auto', I first turn it to Heat, wait 3 seconds, and then turn it to 'Auto', which seems to work well. * Update climate.py * Fixed errors * Fixed comments that were resulting in error. * Update climate.py * Update homeassistant/components/lyric/climate.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/lyric/climate.py Co-authored-by: Martin Hjelmare * Update climate.py I removed a blank line in 268 and another one at the end of the document. I also fixed the outdents of await commands after the _LOGGER.error calls, not sure what else may be driving the flake8 and black errors. Any guidance is much appreciated @MartinHjelmare * Update climate.py * Update climate.py corrected some indents that I think were the culprit of the flake8 errors * Update climate.py I used VS Code to fix locate the flake8 errors. I ran black on it, so I'm hoping that will fix the last lingering black error. Co-authored-by: Martin Hjelmare --- homeassistant/components/lyric/climate.py | 94 ++++++++++++++++++----- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index e798aca5831..dfe88048ba4 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -1,6 +1,7 @@ """Support for Honeywell Lyric climate platform.""" from __future__ import annotations +import asyncio import logging from time import localtime, strftime, time @@ -22,6 +23,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE @@ -45,7 +47,11 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +# Only LCC models support presets +SUPPORT_FLAGS_LCC = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE_RANGE +) +SUPPORT_FLAGS_TCC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE LYRIC_HVAC_ACTION_OFF = "EquipmentOff" LYRIC_HVAC_ACTION_HEAT = "Heat" @@ -166,7 +172,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - return SUPPORT_FLAGS + if self.device.changeableValues.thermostatSetpointStatus: + support_flags = SUPPORT_FLAGS_LCC + else: + support_flags = SUPPORT_FLAGS_TCC + return support_flags @property def temperature_unit(self) -> str: @@ -200,25 +210,28 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" device = self.device - if not device.hasDualSetpointStatus: + if ( + not device.changeableValues.autoChangeoverActive + and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF + ): if self.hvac_mode == HVAC_MODE_COOL: return device.changeableValues.coolSetpoint return device.changeableValues.heatSetpoint return None @property - def target_temperature_low(self) -> float | None: - """Return the upper bound temperature we try to reach.""" + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach.""" device = self.device - if device.hasDualSetpointStatus: + if device.changeableValues.autoChangeoverActive: return device.changeableValues.coolSetpoint return None @property - def target_temperature_high(self) -> float | None: - """Return the upper bound temperature we try to reach.""" + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach.""" device = self.device - if device.hasDualSetpointStatus: + if device.changeableValues.autoChangeoverActive: return device.changeableValues.heatSetpoint return None @@ -256,11 +269,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" + device = self.device target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - device = self.device - if device.hasDualSetpointStatus: + if device.changeableValues.autoChangeoverActive: if target_temp_low is None or target_temp_high is None: raise HomeAssistantError( "Could not find target_temp_low and/or target_temp_high in arguments" @@ -270,11 +283,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): await self._update_thermostat( self.location, device, - coolSetpoint=target_temp_low, - heatSetpoint=target_temp_high, + coolSetpoint=target_temp_high, + heatSetpoint=target_temp_low, + mode=HVAC_MODES[device.changeableValues.heatCoolMode], ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) + await self.coordinator.async_refresh() else: temp = kwargs.get(ATTR_TEMPERATURE) _LOGGER.debug("Set temperature: %s", temp) @@ -289,15 +304,58 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) - await self.coordinator.async_refresh() + await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" - _LOGGER.debug("Set hvac mode: %s", hvac_mode) + _LOGGER.debug("HVAC mode: %s", hvac_mode) try: - await self._update_thermostat( - self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode] - ) + if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: + # If the system is off, turn it to Heat first then to Auto, otherwise it turns to + # Auto briefly and then reverts to Off (perhaps related to heatCoolMode). This is the + # behavior that happens with the native app as well, so likely a bug in the api itself + + if HVAC_MODES[self.device.changeableValues.mode] == HVAC_MODE_OFF: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_COOL], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=False, + ) + # Sleep 3 seconds before proceeding + await asyncio.sleep(3) + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=True, + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[self.device.changeableValues.mode], + ) + await self._update_thermostat( + self.location, self.device, autoChangeoverActive=True + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode] + ) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + autoChangeoverActive=False, + ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh() From bc1438531759ff6965badafc545f41a5ec75e57b Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Tue, 15 Mar 2022 22:33:59 +0100 Subject: [PATCH 145/165] Fix finding matrix room that is already joined (#67967) After some debugging, it seems room.canonical_alias contains the room alias that matches the room_id_or_alias value but is not contained in room.aliases (which is empty). As a result, the matrix component thought the room wasn't alread joined, joins again, and this replaces the previous room which had the listener. This resulted in the component callback not being called for new messages in the room. This fixes #66372 --- homeassistant/components/matrix/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 76e2630b26e..03772630a9e 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -243,7 +243,10 @@ class MatrixBot: room.update_aliases() self._aliases_fetched_for.add(room.room_id) - if room_id_or_alias in room.aliases: + if ( + room_id_or_alias in room.aliases + or room_id_or_alias == room.canonical_alias + ): _LOGGER.debug( "Already in room %s (known as %s)", room.room_id, room_id_or_alias ) From 774f2b9b8265f05b358129213312b3151a7d1063 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 22 Mar 2022 04:40:33 +0100 Subject: [PATCH 146/165] Respect disable_new_entities for new device_tracker entities (#68148) --- .../components/device_tracker/config_entry.py | 12 ++++++- .../device_tracker/test_config_entry.py | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index adabd297c55..c9b8534c2bc 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -149,9 +149,19 @@ def _async_register_mac( return # Make sure entity has a config entry and was disabled by the - # default disable logic in the integration. + # default disable logic in the integration and new entities + # are allowed to be added. if ( entity_entry.config_entry_id is None + or ( + ( + config_entry := hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + ) + is not None + and config_entry.pref_disable_new_entities + ) or entity_entry.disabled_by != er.RegistryEntryDisabler.INTEGRATION ): return diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 5134123074e..73b07d31026 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -137,6 +137,39 @@ async def test_register_mac(hass): assert entity_entry_1.disabled_by is None +async def test_register_mac_ignored(hass): + """Test ignoring registering a mac.""" + dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + + config_entry = MockConfigEntry(domain="test", pref_disable_new_entities=True) + config_entry.add_to_hass(hass) + + mac1 = "12:34:56:AB:CD:EF" + + entity_entry_1 = ent_reg.async_get_or_create( + "device_tracker", + "test", + mac1 + "yo1", + original_name="name 1", + config_entry=config_entry, + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + ) + + ce._async_register_mac(hass, "test", mac1, mac1 + "yo1") + + dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, mac1)}, + ) + + await hass.async_block_till_done() + + entity_entry_1 = ent_reg.async_get(entity_entry_1.entity_id) + + assert entity_entry_1.disabled_by == er.RegistryEntryDisabler.INTEGRATION + + async def test_connected_device_registered(hass): """Test dispatch on connected device being registered.""" From 54b7f13a548495b7014c455d2fa62c93193225fa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:57:36 +0100 Subject: [PATCH 147/165] Add missing await [velbus] (#68153) --- homeassistant/components/velbus/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 2e2ceb761a9..4afe02f4637 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -78,4 +78,4 @@ class VelbusCover(VelbusEntity, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self._channel.set_position(100 - kwargs[ATTR_POSITION]) + await self._channel.set_position(100 - kwargs[ATTR_POSITION]) From 7dd9bfa92f39d716da7a78add950f4503da6d46e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:56:08 +0100 Subject: [PATCH 148/165] Fix point by adding authlib constraint (#68176) * Fix point by pinning authlib * Use constraint --- homeassistant/components/point/__init__.py | 2 ++ homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 288c16df14a..99108323187 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -97,6 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: token_saver=token_saver, ) try: + # pylint: disable-next=fixme + # TODO Remove authlib constraint when refactoring this code await session.ensure_active_token() except ConnectTimeout as err: _LOGGER.debug("Connection Timeout") diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 20b3c5badd3..8ec6f3cc67d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -95,3 +95,7 @@ python-socketio>=4.6.0,<5.0 # Constrain multidict to avoid typing issues # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 + +# Required for compatibility with point integration - ensure_active_token +# https://github.com/home-assistant/core/pull/68176 +authlib<1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fe8962e4f1e..a8d1d40a7d5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -124,6 +124,10 @@ python-socketio>=4.6.0,<5.0 # Constrain multidict to avoid typing issues # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 + +# Required for compatibility with point integration - ensure_active_token +# https://github.com/home-assistant/core/pull/68176 +authlib<1.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 38eb007f6332dab2ddb9ffadf7e8171025bc2078 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Mar 2022 18:14:07 +0100 Subject: [PATCH 149/165] Update opensensemap-api to 0.2.0 (#68193) --- homeassistant/components/opensensemap/air_quality.py | 2 +- homeassistant/components/opensensemap/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index b8028431796..5999eb91580 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -43,7 +43,7 @@ async def async_setup_platform( station_id = config[CONF_STATION_ID] session = async_get_clientsession(hass) - osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session)) + osm_api = OpenSenseMapData(OpenSenseMap(station_id, session)) await osm_api.async_update() diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 513cb5ac3da..baf62985448 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -2,7 +2,7 @@ "domain": "opensensemap", "name": "openSenseMap", "documentation": "https://www.home-assistant.io/integrations/opensensemap", - "requirements": ["opensensemap-api==0.1.5"], + "requirements": ["opensensemap-api==0.2.0"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["opensensemap_api"] diff --git a/requirements_all.txt b/requirements_all.txt index 565cdcae1a3..e9879102828 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ openevsewifi==1.1.0 openhomedevice==2.0.1 # homeassistant.components.opensensemap -opensensemap-api==0.1.5 +opensensemap-api==0.2.0 # homeassistant.components.enigma2 openwebifpy==3.2.7 From df5c09e4830b1e8894e55c7a24dd1d21193bcd93 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 17 Mar 2022 08:51:03 +0100 Subject: [PATCH 150/165] Bump renault-api to 0.1.10 (#68260) 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 71e2e7d64b8..d41686f0c1f 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.9" + "renault-api==0.1.10" ], "codeowners": [ "@epenet" diff --git a/requirements_all.txt b/requirements_all.txt index e9879102828..df63a7159c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2097,7 +2097,7 @@ raspyrfm-client==1.2.8 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.9 +renault-api==0.1.10 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ec2c864424..6550c353e05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1301,7 +1301,7 @@ radios==0.1.1 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.9 +renault-api==0.1.10 # homeassistant.components.python_script restrictedpython==5.2 From caee432901788c0817d805d671d153c2ede3849c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 19 Mar 2022 21:27:04 +0100 Subject: [PATCH 151/165] Hue integration: update errors that should be supressed (#68337) --- homeassistant/components/hue/v2/group.py | 8 ++++++++ homeassistant/components/hue/v2/light.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 31c5a502853..948609f4c13 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -42,6 +42,14 @@ ALLOWED_ERRORS = [ 'device (groupedLight) is "soft off", command (on) may not have effect', "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "device (grouped_light) has communication issues, command (.on) may not have effect", + 'device (grouped_light) is "soft off", command (.on) may not have effect' + "device (grouped_light) has communication issues, command (.on.on) may not have effect", + 'device (grouped_light) is "soft off", command (.on.on) may not have effect' + "device (light) has communication issues, command (.on) may not have effect", + 'device (light) is "soft off", command (.on) may not have effect', + "device (light) has communication issues, command (.on.on) may not have effect", + 'device (light) is "soft off", command (.on.on) may not have effect', ] diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index ee40222b083..5b4574c717c 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -39,6 +39,10 @@ from .helpers import ( ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "device (light) has communication issues, command (.on) may not have effect", + 'device (light) is "soft off", command (.on) may not have effect', + "device (light) has communication issues, command (.on.on) may not have effect", + 'device (light) is "soft off", command (.on.on) may not have effect', ] From a8e1f57058c7a7dd7f39470ef672ae5def40ff40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 10:40:00 -1000 Subject: [PATCH 152/165] Filter IPv6 addreses from enphase_envoy discovery (#68362) --- .../components/enphase_envoy/config_flow.py | 3 ++ .../enphase_envoy/translations/en.json | 3 +- .../enphase_envoy/test_config_flow.py | 41 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index fa43cb61ffe..88310579e72 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.util.network import is_ipv4_address from .const import DOMAIN @@ -86,6 +87,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" + if not is_ipv4_address(discovery_info.host): + return self.async_abort(reason="not_ipv4_address") serial = discovery_info.properties["serialnum"] await self.async_set_unique_id(serial) self.ip_address = discovery_info.host diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 5d4617ed9fa..ff600fea454 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Device is already configured", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "not_ipv4_address": "Only IPv4 addresess are supported" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 76179c02e22..caba2296927 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -6,6 +6,7 @@ import httpx from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.enphase_envoy.const import DOMAIN +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -312,8 +313,8 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="1.1.1.1", - addresses=["1.1.1.1"], + host="4.4.4.4", + addresses=["4.4.4.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -324,6 +325,42 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: assert result["type"] == "abort" assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == "4.4.4.4" + + +async def test_zeroconf_serial_already_exists_ignores_ipv6(hass: HomeAssistant) -> None: + """Test serial number already exists from zeroconf but the discovery is ipv6.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + }, + unique_id="1234", + title="Envoy", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234"}, + type="mock_type", + ), + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_ipv4_address" + assert config_entry.data[CONF_HOST] == "1.1.1.1" async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: From 23e9aa6ad246b0818a1d6e83b67b49ed1fa6623c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Mar 2022 14:28:16 -0700 Subject: [PATCH 153/165] Handle Hue discovery errors (#68392) --- homeassistant/components/hue/config_flow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0901d9a1e2c..265777814a8 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -6,6 +6,7 @@ import logging from typing import Any from urllib.parse import urlparse +import aiohttp from aiohue import LinkButtonNotPressed, create_app_key from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp from aiohue.util import normalize_bridge_id @@ -70,9 +71,12 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, host: str, bridge_id: str | None = None ) -> DiscoveredHueBridge: """Return a DiscoveredHueBridge object.""" - bridge = await discover_bridge( - host, websession=aiohttp_client.async_get_clientsession(self.hass) - ) + try: + bridge = await discover_bridge( + host, websession=aiohttp_client.async_get_clientsession(self.hass) + ) + except aiohttp.ClientError: + return None if bridge_id is not None: bridge_id = normalize_bridge_id(bridge_id) assert bridge_id == bridge.id From 3bf0a64e21f436a0b44822a401a390e9e663fcc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Mar 2022 12:41:15 -1000 Subject: [PATCH 154/165] Fix tplink color temp conversion (#68484) --- homeassistant/components/tplink/light.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 6efabe537f7..182bef586ee 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -88,7 +88,10 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): # Handle turning to temp mode if ATTR_COLOR_TEMP in kwargs: - color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) + # Handle temp conversion mireds -> kelvin being slightly outside of valid range + kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) + kelvin_range = self.device.valid_temperature_range + color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin)) _LOGGER.debug("Changing color temp to %s", color_tmp) await self.device.set_color_temp( color_tmp, brightness=brightness, transition=transition From 2be9798fb8ccce629d736ba1b7b239706be972cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Mar 2022 20:42:13 -0700 Subject: [PATCH 155/165] Bumped version to 2022.3.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 36f5c21b279..224bf89b0e2 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 = 3 -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 5e229a0da9d..c4e7a968cc2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.5 +version = 2022.3.6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From f85781dc519842bbdafaa55203a096550e377f46 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Mar 2022 18:45:27 +0100 Subject: [PATCH 156/165] Fix targeting all or none entities in service calls (#68513) * Fix targeting all or none entities in service calls * Add test --- homeassistant/helpers/service.py | 9 ++++++--- tests/helpers/test_service.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e638288a58c..2fee7f0716f 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -218,9 +218,12 @@ def async_prepare_call_from_config( if CONF_ENTITY_ID in target: registry = entity_registry.async_get(hass) - target[CONF_ENTITY_ID] = entity_registry.async_resolve_entity_ids( - registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) - ) + entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) + if entity_ids not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE): + entity_ids = entity_registry.async_resolve_entity_ids( + registry, entity_ids + ) + target[CONF_ENTITY_ID] = entity_ids except TemplateError as ex: raise HomeAssistantError( f"Error rendering service target template: {ex}" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 54507d9b3bd..649f2b42368 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -397,6 +397,22 @@ async def test_service_call_entry_id(hass): assert dict(calls[0].data) == {"entity_id": ["hello.world"]} +@pytest.mark.parametrize("target", ("all", "none")) +async def test_service_call_all_none(hass, target): + """Test service call targeting all.""" + calls = async_mock_service(hass, "test_domain", "test_service") + + config = { + "service": "test_domain.test_service", + "target": {"entity_id": target}, + } + + await service.async_call_from_config(hass, config) + await hass.async_block_till_done() + + assert dict(calls[0].data) == {"entity_id": target} + + async def test_extract_entity_ids(hass): """Test extract_entity_ids method.""" hass.states.async_set("light.Bowl", STATE_ON) From 9636435ff2912e0d0c9be7b2c7ecd63ae25c2529 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Tue, 22 Mar 2022 18:14:01 -0400 Subject: [PATCH 157/165] Add support for general API exception in Sense integration (#68517) --- .../components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/config_flow.py | 6 ++--- homeassistant/components/sense/const.py | 12 ++++++++-- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sense/test_config_flow.py | 22 ++++++++++++++++++- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index c11fecb3ff1..c7f9175bc5e 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.10.2"], + "requirements": ["sense_energy==0.10.3"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index eea36424662..8769d4cb83f 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS +from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS _LOGGER = logging.getLogger(__name__) @@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.validate_input(user_input) except SenseMFARequiredException: return await self.async_step_validation() - except SENSE_TIMEOUT_EXCEPTIONS: + except SENSE_CONNECT_EXCEPTIONS: errors["base"] = "cannot_connect" except SenseAuthenticationException: errors["base"] = "invalid_auth" @@ -93,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input: try: await self._gateway.validate_mfa(user_input[CONF_CODE]) - except SENSE_TIMEOUT_EXCEPTIONS: + except SENSE_CONNECT_EXCEPTIONS: errors["base"] = "cannot_connect" except SenseAuthenticationException: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index bb323151950..622e9897a66 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -3,8 +3,11 @@ import asyncio import socket -from sense_energy import SenseAPITimeoutException -from sense_energy.sense_exceptions import SenseWebsocketException +from sense_energy import ( + SenseAPIException, + SenseAPITimeoutException, + SenseWebsocketException, +) DOMAIN = "sense" DEFAULT_TIMEOUT = 10 @@ -40,6 +43,11 @@ ICON = "mdi:flash" SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException) +SENSE_CONNECT_EXCEPTIONS = ( + asyncio.TimeoutError, + SenseAPITimeoutException, + SenseAPIException, +) MDI_ICONS = { "ac": "air-conditioner", diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 30de722a7bc..04b0b451db4 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.10.2"], + "requirements": ["sense_energy==0.10.3"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index df63a7159c7..085175ad22d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2179,7 +2179,7 @@ sense-hat==2.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.2 +sense_energy==0.10.3 # homeassistant.components.sentry sentry-sdk==1.5.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6550c353e05..821fc3bdfcc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1344,7 +1344,7 @@ screenlogicpy==0.5.4 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.2 +sense_energy==0.10.3 # homeassistant.components.sentry sentry-sdk==1.5.5 diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index f939142aee4..690e5d2e530 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest from sense_energy import ( + SenseAPIException, SenseAPITimeoutException, SenseAuthenticationException, SenseMFARequiredException, @@ -189,7 +190,7 @@ async def test_form_mfa_required_exception(hass, mock_sense): assert result3["errors"] == {"base": "unknown"} -async def test_form_cannot_connect(hass): +async def test_form_timeout(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -208,6 +209,25 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "sense_energy.ASyncSenseable.authenticate", + side_effect=SenseAPIException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"timeout": "6", "email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + async def test_form_unknown_exception(hass): """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( From 454cb44ee890374acba0d497600cbe35e3861214 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Mar 2022 16:20:20 -0500 Subject: [PATCH 158/165] Add cooldown timer before Sonos resubscriptions (#68521) --- homeassistant/components/sonos/speaker.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 3a2bac51684..08efa3571c6 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -63,6 +63,7 @@ from .media import SonosMedia from .statistics import ActivityStatistics, EventStatistics NEVER_TIME = -1200.0 +RESUB_COOLDOWN_SECONDS = 10.0 EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, @@ -126,6 +127,7 @@ class SonosSpeaker: self._last_event_cache: dict[str, Any] = {} self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name) self.event_stats: EventStatistics = EventStatistics(self.zone_name) + self._resub_cooldown_expires_at: float | None = None # Scheduled callback handles self._poll_timer: Callable | None = None @@ -502,6 +504,16 @@ class SonosSpeaker: @callback def speaker_activity(self, source): """Track the last activity on this speaker, set availability and resubscribe.""" + if self._resub_cooldown_expires_at: + if time.monotonic() < self._resub_cooldown_expires_at: + _LOGGER.debug( + "Activity on %s from %s while in cooldown, ignoring", + self.zone_name, + source, + ) + return + self._resub_cooldown_expires_at = None + _LOGGER.debug("Activity on %s from %s", self.zone_name, source) self._last_activity = time.monotonic() self.activity_stats.activity(source, self._last_activity) @@ -542,6 +554,10 @@ class SonosSpeaker: if not self.available: return + if self._resub_cooldown_expires_at is None and not self.hass.is_stopping: + self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS + _LOGGER.debug("Starting resubscription cooldown for %s", self.zone_name) + self.available = False self.async_write_entity_states() From 1114877062cabb3859b6c0fab5f2b02894143360 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Mar 2022 02:16:19 -0700 Subject: [PATCH 159/165] Hue handle HTTP errors (#68396) --- homeassistant/components/hue/bridge.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 346cc67d235..dd6182b244a 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -6,6 +6,7 @@ from collections.abc import Callable import logging from typing import Any +import aiohttp from aiohttp import client_exceptions from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized from aiohue.errors import AiohueException, BridgeBusy @@ -14,7 +15,7 @@ import async_timeout from homeassistant import core from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import CONF_API_VERSION, DOMAIN @@ -131,7 +132,11 @@ class HueBridge: # log only self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) return None - raise err + raise HomeAssistantError(f"Request failed: {err}") from err + except aiohttp.ClientError as err: + raise HomeAssistantError( + f"Request failed due connection error: {err}" + ) from err async def async_reset(self) -> bool: """Reset this bridge to default state. From 050600375d7290a3bec64eec818a1af1099e3b7e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 05:59:06 +0100 Subject: [PATCH 160/165] Simplify Hue error handling a bit (#68529) --- homeassistant/components/hue/bridge.py | 19 +++++++---------- homeassistant/components/hue/v2/group.py | 27 ++---------------------- homeassistant/components/hue/v2/light.py | 11 ---------- tests/components/hue/conftest.py | 2 +- 4 files changed, 11 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index dd6182b244a..e15da5c8489 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -117,22 +117,19 @@ class HueBridge: self.authorized = True return True - async def async_request_call( - self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs - ) -> Any: - """Send request to the Hue bridge, optionally omitting error(s).""" + async def async_request_call(self, task: Callable, *args, **kwargs) -> Any: + """Send request to the Hue bridge.""" try: return await task(*args, **kwargs) except AiohueException as err: - # The (new) Hue api can be a bit fanatic with throwing errors - # some of which we accept in certain conditions - # handle that here. Note that these errors are strings and do not have - # an identifier or something. - if allowed_errors is not None and str(err) in allowed_errors: + # The (new) Hue api can be a bit fanatic with throwing errors so + # we have some logic to treat some responses as warning only. + msg = f"Request failed: {err}" + if "may not have effect" in str(err): # log only - self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) + self.logger.debug(msg) return None - raise HomeAssistantError(f"Request failed: {err}") from err + raise HomeAssistantError(msg) from err except aiohttp.ClientError as err: raise HomeAssistantError( f"Request failed due connection error: {err}" diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 948609f4c13..162ef58d320 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -37,21 +37,6 @@ from .helpers import ( normalize_hue_transition, ) -ALLOWED_ERRORS = [ - "device (groupedLight) has communication issues, command (on) may not have effect", - 'device (groupedLight) is "soft off", command (on) may not have effect', - "device (light) has communication issues, command (on) may not have effect", - 'device (light) is "soft off", command (on) may not have effect', - "device (grouped_light) has communication issues, command (.on) may not have effect", - 'device (grouped_light) is "soft off", command (.on) may not have effect' - "device (grouped_light) has communication issues, command (.on.on) may not have effect", - 'device (grouped_light) is "soft off", command (.on.on) may not have effect' - "device (light) has communication issues, command (.on) may not have effect", - 'device (light) is "soft off", command (.on) may not have effect', - "device (light) has communication issues, command (.on.on) may not have effect", - 'device (light) is "soft off", command (.on.on) may not have effect', -] - async def async_setup_entry( hass: HomeAssistant, @@ -183,10 +168,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): and flash is None ): await self.bridge.async_request_call( - self.controller.set_state, - id=self.resource.id, - on=True, - allowed_errors=ALLOWED_ERRORS, + self.controller.set_state, id=self.resource.id, on=True ) return @@ -202,7 +184,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity): color_xy=xy_color if light.supports_color else None, color_temp=color_temp if light.supports_color_temperature else None, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) for light in self.controller.get_lights(self.resource.id) ] @@ -222,10 +203,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # To set other features, you'll have to control the attached lights if transition is None: await self.bridge.async_request_call( - self.controller.set_state, - id=self.resource.id, - on=False, - allowed_errors=ALLOWED_ERRORS, + self.controller.set_state, id=self.resource.id, on=False ) return @@ -237,7 +215,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity): light.id, on=False, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) for light in self.controller.get_lights(self.resource.id) ] diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 5b4574c717c..999da408102 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -36,15 +36,6 @@ from .helpers import ( normalize_hue_transition, ) -ALLOWED_ERRORS = [ - "device (light) has communication issues, command (on) may not have effect", - 'device (light) is "soft off", command (on) may not have effect', - "device (light) has communication issues, command (.on) may not have effect", - 'device (light) is "soft off", command (.on) may not have effect', - "device (light) has communication issues, command (.on.on) may not have effect", - 'device (light) is "soft off", command (.on.on) may not have effect', -] - async def async_setup_entry( hass: HomeAssistant, @@ -182,7 +173,6 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) async def async_turn_off(self, **kwargs: Any) -> None: @@ -202,7 +192,6 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, on=False, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) async def async_set_flash(self, flash: str) -> None: diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index d0d15d320e0..d730d3f18f5 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -60,7 +60,7 @@ def create_mock_bridge(hass, api_version=1): bridge.async_initialize_bridge = async_initialize_bridge - async def async_request_call(task, *args, allowed_errors=None, **kwargs): + async def async_request_call(task, *args, **kwargs): await task(*args, **kwargs) bridge.async_request_call = async_request_call From 6fd43553141f5b5ab7063a09a62d1f13c03bdd52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Mar 2022 09:38:34 -1000 Subject: [PATCH 161/165] Filter IPv6 addresses from AppleTV zeroconf discovery (#68530) --- .../components/apple_tv/config_flow.py | 3 +++ .../components/apple_tv/strings.json | 1 + .../components/apple_tv/translations/en.json | 7 ++----- tests/components/apple_tv/test_config_flow.py | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index c4853359cc6..8e8e6006895 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.network import is_ipv6_address from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN @@ -166,6 +167,8 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle device found via zeroconf.""" host = discovery_info.host + if is_ipv6_address(host): + return self.async_abort(reason="ipv6_not_supported") self._async_abort_entries_match({CONF_ADDRESS: host}) service_type = discovery_info.type[:-1] # Remove leading . name = discovery_info.name.replace(f".{service_type}.", "") diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index 3ea47ba3d8a..e25c596f786 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -48,6 +48,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "ipv6_not_supported": "IPv6 is not supported.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index db16129ca2a..f455d590d79 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -2,13 +2,12 @@ "config": { "abort": { "already_configured": "Device is already configured", - "already_configured_device": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", "device_not_found": "Device was not found during discovery, please try adding it again.", "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.", - "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "ipv6_not_supported": "IPv6 is not supported.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful", "setup_failed": "Failed to set up device.", @@ -18,7 +17,6 @@ "already_configured": "Device is already configured", "invalid_auth": "Invalid authentication", "no_devices_found": "No devices found on the network", - "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", "unknown": "Unexpected error" }, "flow_title": "{name} ({type})", @@ -72,6 +70,5 @@ "description": "Configure general device settings" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index ca617026d94..6efb4820564 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1066,3 +1066,22 @@ async def test_option_start_off(hass): assert result2["type"] == "create_entry" assert config_entry.options[CONF_START_OFF] + + +async def test_zeroconf_rejects_ipv6(hass): + """Test zeroconf discovery rejects ipv6.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + port=None, + type="_touch-able._tcp.local.", + name="dmapid._touch-able._tcp.local.", + properties={"CtlN": "Apple TV"}, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "ipv6_not_supported" From 85e6b3950ca4335d59dd6d00ce0bda520cadbf54 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 13:03:14 +0100 Subject: [PATCH 162/165] Bump aiohue to 4.4.0 (#68556) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/fixtures/v2_resources.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index bf6e7f06abd..e8f91539b5c 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.3.0"], + "requirements": ["aiohue==4.4.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 085175ad22d..8f8a1be0915 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.3.0 +aiohue==4.4.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 821fc3bdfcc..fc4a10c371a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.3.0 +aiohue==4.4.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 806dcecfacf..c3f03d8f48a 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -460,7 +460,7 @@ "model_id": "BSB002", "product_archetype": "bridge_v2", "product_name": "Philips hue", - "software_version": "1.48.1948086000" + "software_version": "1.50.1950111030" }, "services": [ { From 53fa6c138a287c21bcd747ea54f20421f62afc36 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 20:18:06 +0100 Subject: [PATCH 163/165] Bump aiohue to version 4.4.1 (#68579) --- 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 e8f91539b5c..d3b492f3b9e 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.4.0"], + "requirements": ["aiohue==4.4.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 8f8a1be0915..e550afb26d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.0 +aiohue==4.4.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc4a10c371a..e29d148d01f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.0 +aiohue==4.4.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 From 911de9434580f27cd8ca8d0352fd9adab320234c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Mar 2022 15:47:05 -0700 Subject: [PATCH 164/165] Bumped version to 2022.3.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 224bf89b0e2..1d123a851b7 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 = 3 -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 c4e7a968cc2..c08f865b17a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.6 +version = 2022.3.7 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From be5d816fbe34e07f00484b34bcefa65c1896258f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 24 Mar 2022 02:07:45 +0100 Subject: [PATCH 165/165] Bump py-synologydsm-api to 1.0.7 (#68584) --- 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 39eb1190388..1c9df126a89 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.6"], + "requirements": ["py-synologydsm-api==1.0.7"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e550afb26d9..a5e7ea3852d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1338,7 +1338,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.6 +py-synologydsm-api==1.0.7 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e29d148d01f..3d4c200d682 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.6 +py-synologydsm-api==1.0.7 # homeassistant.components.seventeentrack py17track==2021.12.2