From 596f19055e64ee0cf93fa0cb964db38a05886aa3 Mon Sep 17 00:00:00 2001 From: Paarth Shah Date: Tue, 16 Jan 2024 01:07:51 -0800 Subject: [PATCH 01/29] Fix MatrixBot not resolving room aliases per-command (#106347) --- homeassistant/components/matrix/__init__.py | 7 +- tests/components/matrix/conftest.py | 72 ++++++-- tests/components/matrix/test_commands.py | 180 ++++++++++++++++++++ tests/components/matrix/test_matrix_bot.py | 66 +------ 4 files changed, 246 insertions(+), 79 deletions(-) create mode 100644 tests/components/matrix/test_commands.py diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 44a65a2de59..e91ee4d270c 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -223,9 +223,12 @@ class MatrixBot: def _load_commands(self, commands: list[ConfigCommand]) -> None: for command in commands: # Set the command for all listening_rooms, unless otherwise specified. - command.setdefault(CONF_ROOMS, list(self._listening_rooms.values())) + if rooms := command.get(CONF_ROOMS): + command[CONF_ROOMS] = [self._listening_rooms[room] for room in rooms] + else: + command[CONF_ROOMS] = list(self._listening_rooms.values()) - # COMMAND_SCHEMA guarantees that exactly one of CONF_WORD and CONF_expression are set. + # COMMAND_SCHEMA guarantees that exactly one of CONF_WORD and CONF_EXPRESSION are set. if (word_command := command.get(CONF_WORD)) is not None: for room_id in command[CONF_ROOMS]: self._word_commands.setdefault(room_id, {}) diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index 1198d7e6012..3e7d4833d6f 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -31,6 +31,8 @@ from homeassistant.components.matrix import ( CONF_WORD, EVENT_MATRIX_COMMAND, MatrixBot, + RoomAlias, + RoomAnyID, RoomID, ) from homeassistant.components.matrix.const import DOMAIN as MATRIX_DOMAIN @@ -51,13 +53,15 @@ from tests.common import async_capture_events TEST_NOTIFIER_NAME = "matrix_notify" TEST_HOMESERVER = "example.com" -TEST_DEFAULT_ROOM = "!DefaultNotificationRoom:example.com" -TEST_ROOM_A_ID = "!RoomA-ID:example.com" -TEST_ROOM_B_ID = "!RoomB-ID:example.com" -TEST_ROOM_B_ALIAS = "#RoomB-Alias:example.com" -TEST_JOINABLE_ROOMS = { +TEST_DEFAULT_ROOM = RoomID("!DefaultNotificationRoom:example.com") +TEST_ROOM_A_ID = RoomID("!RoomA-ID:example.com") +TEST_ROOM_B_ID = RoomID("!RoomB-ID:example.com") +TEST_ROOM_B_ALIAS = RoomAlias("#RoomB-Alias:example.com") +TEST_ROOM_C_ID = RoomID("!RoomC-ID:example.com") +TEST_JOINABLE_ROOMS: dict[RoomAnyID, RoomID] = { TEST_ROOM_A_ID: TEST_ROOM_A_ID, TEST_ROOM_B_ALIAS: TEST_ROOM_B_ID, + TEST_ROOM_C_ID: TEST_ROOM_C_ID, } TEST_BAD_ROOM = "!UninvitedRoom:example.com" TEST_MXID = "@user:example.com" @@ -74,7 +78,7 @@ class _MockAsyncClient(AsyncClient): async def close(self): return None - async def room_resolve_alias(self, room_alias: str): + async def room_resolve_alias(self, room_alias: RoomAnyID): if room_id := TEST_JOINABLE_ROOMS.get(room_alias): return RoomResolveAliasResponse( room_alias=room_alias, room_id=room_id, servers=[TEST_HOMESERVER] @@ -150,6 +154,16 @@ MOCK_CONFIG_DATA = { CONF_EXPRESSION: "My name is (?P.*)", CONF_NAME: "ExpressionTriggerEventName", }, + { + CONF_WORD: "WordTriggerSubset", + CONF_NAME: "WordTriggerSubsetEventName", + CONF_ROOMS: [TEST_ROOM_B_ALIAS, TEST_ROOM_C_ID], + }, + { + CONF_EXPRESSION: "Your name is (?P.*)", + CONF_NAME: "ExpressionTriggerSubsetEventName", + CONF_ROOMS: [TEST_ROOM_B_ALIAS, TEST_ROOM_C_ID], + }, ], }, NOTIFY_DOMAIN: { @@ -164,15 +178,32 @@ MOCK_WORD_COMMANDS = { "WordTrigger": { "word": "WordTrigger", "name": "WordTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], + "rooms": list(TEST_JOINABLE_ROOMS.values()), } }, TEST_ROOM_B_ID: { "WordTrigger": { "word": "WordTrigger", "name": "WordTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], - } + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + "WordTriggerSubset": { + "word": "WordTriggerSubset", + "name": "WordTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, + }, + TEST_ROOM_C_ID: { + "WordTrigger": { + "word": "WordTrigger", + "name": "WordTriggerEventName", + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + "WordTriggerSubset": { + "word": "WordTriggerSubset", + "name": "WordTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, }, } @@ -181,15 +212,32 @@ MOCK_EXPRESSION_COMMANDS = { { "expression": re.compile("My name is (?P.*)"), "name": "ExpressionTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], + "rooms": list(TEST_JOINABLE_ROOMS.values()), } ], TEST_ROOM_B_ID: [ { "expression": re.compile("My name is (?P.*)"), "name": "ExpressionTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], - } + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + { + "expression": re.compile("Your name is (?P.*)"), + "name": "ExpressionTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, + ], + TEST_ROOM_C_ID: [ + { + "expression": re.compile("My name is (?P.*)"), + "name": "ExpressionTriggerEventName", + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + { + "expression": re.compile("Your name is (?P.*)"), + "name": "ExpressionTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, ], } diff --git a/tests/components/matrix/test_commands.py b/tests/components/matrix/test_commands.py new file mode 100644 index 00000000000..cbf85ccc597 --- /dev/null +++ b/tests/components/matrix/test_commands.py @@ -0,0 +1,180 @@ +"""Test MatrixBot's ability to parse and respond to commands in matrix rooms.""" +from functools import partial +from itertools import chain +from typing import Any + +from nio import MatrixRoom, RoomMessageText +from pydantic.dataclasses import dataclass +import pytest + +from homeassistant.components.matrix import MatrixBot, RoomID +from homeassistant.core import Event, HomeAssistant + +from tests.components.matrix.conftest import ( + MOCK_EXPRESSION_COMMANDS, + MOCK_WORD_COMMANDS, + TEST_MXID, + TEST_ROOM_A_ID, + TEST_ROOM_B_ID, + TEST_ROOM_C_ID, +) + +ALL_ROOMS = (TEST_ROOM_A_ID, TEST_ROOM_B_ID, TEST_ROOM_C_ID) +SUBSET_ROOMS = (TEST_ROOM_B_ID, TEST_ROOM_C_ID) + + +@dataclass +class CommandTestParameters: + """Dataclass of parameters representing the command config parameters and expected result state. + + Switches behavior based on `room_id` and `expected_event_room_data`. + """ + + room_id: RoomID + room_message: RoomMessageText + expected_event_data_extra: dict[str, Any] | None + + @property + def expected_event_data(self) -> dict[str, Any] | None: + """Fully-constructed expected event data. + + Commands that are named with 'Subset' are expected not to be read from Room A. + """ + + if ( + self.expected_event_data_extra is None + or "Subset" in self.expected_event_data_extra["command"] + and self.room_id not in SUBSET_ROOMS + ): + return None + return { + "sender": "@SomeUser:example.com", + "room": self.room_id, + } | self.expected_event_data_extra + + +room_message_base = partial( + RoomMessageText, + formatted_body=None, + format=None, + source={ + "event_id": "fake_event_id", + "sender": "@SomeUser:example.com", + "origin_server_ts": 123456789, + }, +) +word_command_global = partial( + CommandTestParameters, + room_message=room_message_base(body="!WordTrigger arg1 arg2"), + expected_event_data_extra={ + "command": "WordTriggerEventName", + "args": ["arg1", "arg2"], + }, +) +expr_command_global = partial( + CommandTestParameters, + room_message=room_message_base(body="My name is FakeName"), + expected_event_data_extra={ + "command": "ExpressionTriggerEventName", + "args": {"name": "FakeName"}, + }, +) +word_command_subset = partial( + CommandTestParameters, + room_message=room_message_base(body="!WordTriggerSubset arg1 arg2"), + expected_event_data_extra={ + "command": "WordTriggerSubsetEventName", + "args": ["arg1", "arg2"], + }, +) +expr_command_subset = partial( + CommandTestParameters, + room_message=room_message_base(body="Your name is FakeName"), + expected_event_data_extra={ + "command": "ExpressionTriggerSubsetEventName", + "args": {"name": "FakeName"}, + }, +) +# Messages without commands should trigger nothing +fake_command_global = partial( + CommandTestParameters, + room_message=room_message_base(body="This is not a real command!"), + expected_event_data_extra=None, +) +# Valid commands sent by the bot user should trigger nothing +self_command_global = partial( + CommandTestParameters, + room_message=room_message_base( + body="!WordTrigger arg1 arg2", + source={ + "event_id": "fake_event_id", + "sender": TEST_MXID, + "origin_server_ts": 123456789, + }, + ), + expected_event_data_extra=None, +) + + +@pytest.mark.parametrize( + "command_params", + chain( + (word_command_global(room_id) for room_id in ALL_ROOMS), + (expr_command_global(room_id) for room_id in ALL_ROOMS), + (word_command_subset(room_id) for room_id in SUBSET_ROOMS), + (expr_command_subset(room_id) for room_id in SUBSET_ROOMS), + ), +) +async def test_commands( + hass: HomeAssistant, + matrix_bot: MatrixBot, + command_events: list[Event], + command_params: CommandTestParameters, +): + """Test that the configured commands are used correctly.""" + room = MatrixRoom(room_id=command_params.room_id, own_user_id=matrix_bot._mx_id) + + await hass.async_start() + assert len(command_events) == 0 + await matrix_bot._handle_room_message(room, command_params.room_message) + await hass.async_block_till_done() + + # MatrixBot should emit exactly one Event with matching data from this Command + assert len(command_events) == 1 + event = command_events[0] + assert event.data == command_params.expected_event_data + + +@pytest.mark.parametrize( + "command_params", + chain( + (word_command_subset(TEST_ROOM_A_ID),), + (expr_command_subset(TEST_ROOM_A_ID),), + (fake_command_global(room_id) for room_id in ALL_ROOMS), + (self_command_global(room_id) for room_id in ALL_ROOMS), + ), +) +async def test_non_commands( + hass: HomeAssistant, + matrix_bot: MatrixBot, + command_events: list[Event], + command_params: CommandTestParameters, +): + """Test that normal/non-qualifying messages don't wrongly trigger commands.""" + room = MatrixRoom(room_id=command_params.room_id, own_user_id=matrix_bot._mx_id) + + await hass.async_start() + assert len(command_events) == 0 + await matrix_bot._handle_room_message(room, command_params.room_message) + await hass.async_block_till_done() + + # MatrixBot should not treat this message as a Command + assert len(command_events) == 0 + + +async def test_commands_parsing(hass: HomeAssistant, matrix_bot: MatrixBot): + """Test that the configured commands were parsed correctly.""" + + await hass.async_start() + assert matrix_bot._word_commands == MOCK_WORD_COMMANDS + assert matrix_bot._expression_commands == MOCK_EXPRESSION_COMMANDS diff --git a/tests/components/matrix/test_matrix_bot.py b/tests/components/matrix/test_matrix_bot.py index 0048f6665e8..bfd6d5824cb 100644 --- a/tests/components/matrix/test_matrix_bot.py +++ b/tests/components/matrix/test_matrix_bot.py @@ -1,5 +1,4 @@ """Configure and test MatrixBot.""" -from nio import MatrixRoom, RoomMessageText from homeassistant.components.matrix import ( DOMAIN as MATRIX_DOMAIN, @@ -9,12 +8,7 @@ from homeassistant.components.matrix import ( from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant -from .conftest import ( - MOCK_EXPRESSION_COMMANDS, - MOCK_WORD_COMMANDS, - TEST_NOTIFIER_NAME, - TEST_ROOM_A_ID, -) +from .conftest import TEST_NOTIFIER_NAME async def test_services(hass: HomeAssistant, matrix_bot: MatrixBot): @@ -29,61 +23,3 @@ async def test_services(hass: HomeAssistant, matrix_bot: MatrixBot): # Verify that the matrix notifier is registered assert (notify_service := services.get(NOTIFY_DOMAIN)) assert TEST_NOTIFIER_NAME in notify_service - - -async def test_commands(hass, matrix_bot: MatrixBot, command_events): - """Test that the configured commands were parsed correctly.""" - - await hass.async_start() - assert len(command_events) == 0 - - assert matrix_bot._word_commands == MOCK_WORD_COMMANDS - assert matrix_bot._expression_commands == MOCK_EXPRESSION_COMMANDS - - room_id = TEST_ROOM_A_ID - room = MatrixRoom(room_id=room_id, own_user_id=matrix_bot._mx_id) - - # Test single-word command. - word_command_message = RoomMessageText( - body="!WordTrigger arg1 arg2", - formatted_body=None, - format=None, - source={ - "event_id": "fake_event_id", - "sender": "@SomeUser:example.com", - "origin_server_ts": 123456789, - }, - ) - await matrix_bot._handle_room_message(room, word_command_message) - await hass.async_block_till_done() - assert len(command_events) == 1 - event = command_events.pop() - assert event.data == { - "command": "WordTriggerEventName", - "sender": "@SomeUser:example.com", - "room": room_id, - "args": ["arg1", "arg2"], - } - - # Test expression command. - room = MatrixRoom(room_id=room_id, own_user_id=matrix_bot._mx_id) - expression_command_message = RoomMessageText( - body="My name is FakeName", - formatted_body=None, - format=None, - source={ - "event_id": "fake_event_id", - "sender": "@SomeUser:example.com", - "origin_server_ts": 123456789, - }, - ) - await matrix_bot._handle_room_message(room, expression_command_message) - await hass.async_block_till_done() - assert len(command_events) == 1 - event = command_events.pop() - assert event.data == { - "command": "ExpressionTriggerEventName", - "sender": "@SomeUser:example.com", - "room": room_id, - "args": {"name": "FakeName"}, - } From a8be7c27ad254538fc2f74f791323be143edb053 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:34:25 +0100 Subject: [PATCH 02/29] Bump Pyenphase to 1.16.0 (#107719) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 4ae7760a56b..67d07f0d502 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.15.2"], + "requirements": ["pyenphase==1.16.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 872a05ee8fb..55721ae3b9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.15.2 +pyenphase==1.16.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df239a77e63..2697cdcb8b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1328,7 +1328,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.15.2 +pyenphase==1.16.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 5c99c6e823261db1ac449e363b8c15e610b46215 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 16 Jan 2024 09:23:04 +0000 Subject: [PATCH 03/29] Fix loading empty yaml files with include_dir_named (#107853) --- homeassistant/util/yaml/loader.py | 5 +---- tests/util/yaml/test_init.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 60e917a6a99..5da5a84cc48 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -355,10 +355,7 @@ def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> NodeDi filename = os.path.splitext(os.path.basename(fname))[0] if os.path.basename(fname) == SECRET_YAML: continue - loaded_yaml = load_yaml(fname, loader.secrets) - if loaded_yaml is None: - continue - mapping[filename] = loaded_yaml + mapping[filename] = load_yaml(fname, loader.secrets) return _add_reference(mapping, loader, node) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 1e31d8c6955..30637fe2785 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -193,7 +193,7 @@ def test_include_dir_list_recursive( ), ( {"/test/first.yaml": "1", "/test/second.yaml": None}, - {"first": 1}, + {"first": 1, "second": None}, ), ], ) From e2ef8896870a41e295e9205fa88145b6df3bd61e Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:10:54 +0100 Subject: [PATCH 04/29] Bump openwebifpy to 4.2.1 (#107894) --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 42fbcb5b9bc..e298b3b714f 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/enigma2", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.0.4"] + "requirements": ["openwebifpy==4.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 55721ae3b9b..76a2a230e27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1425,7 +1425,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.0.4 +openwebifpy==4.2.1 # homeassistant.components.luci openwrt-luci-rpc==1.1.16 From c0c9fb0f008efe09c221e0bb7ffeefbb4057d1c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Jan 2024 07:27:52 -1000 Subject: [PATCH 05/29] Bump aiohomekit to 3.1.3 (#107929) changelog: https://github.com/Jc2k/aiohomekit/compare/3.1.2...3.1.3 fixes maybe #97888 --- 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 4af79a6f811..799058b0e20 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.1.2"], + "requirements": ["aiohomekit==3.1.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 76a2a230e27..7da414d2742 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.2 +aiohomekit==3.1.3 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2697cdcb8b0..713c4806606 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.2 +aiohomekit==3.1.3 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From 80387be06130ba9886f7687ba6845aad6956147e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 13 Jan 2024 14:34:24 +0100 Subject: [PATCH 06/29] Skip disk types in System Monitor (#107943) * Skip disk types in System Monitor * change back --- homeassistant/components/systemmonitor/util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 742e0d40f3d..aeb7816784b 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -7,6 +7,8 @@ import psutil _LOGGER = logging.getLogger(__name__) +SKIP_DISK_TYPES = {"proc", "tmpfs", "devtmpfs"} + def get_all_disk_mounts() -> set[str]: """Return all disk mount points on system.""" @@ -18,6 +20,9 @@ def get_all_disk_mounts() -> set[str]: # ENOENT, pop-up a Windows GUI error for a non-ready # partition or just hang. continue + if part.fstype in SKIP_DISK_TYPES: + # Ignore disks which are memory + continue try: usage = psutil.disk_usage(part.mountpoint) except PermissionError: From 488acc325231de4b184e032445d95f9db59875d1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 13 Jan 2024 13:48:02 +0100 Subject: [PATCH 07/29] Fix duplicate unique id in System Monitor (again) (#107947) Fix duplicate unique id in System Monitor --- homeassistant/components/systemmonitor/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index da6e35238ec..95437c7fa4c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -405,7 +405,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.add(f"{_type}_{slugify(argument)}") + loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( sensor_registry, @@ -425,7 +425,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.add(f"{_type}_{slugify(argument)}") + loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( sensor_registry, @@ -449,7 +449,7 @@ async def async_setup_entry( sensor_registry[(_type, argument)] = SensorData( argument, None, None, None, None ) - loaded_resources.add(f"{_type}_{slugify(argument)}") + loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( sensor_registry, From 9551ff31ec10d0c3e0988b17deaef1d0bc304834 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:53:48 +0100 Subject: [PATCH 08/29] Bump pyenphase to 1.17.0 (#107950) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 67d07f0d502..4b3a4eadb3d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.16.0"], + "requirements": ["pyenphase==1.17.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 7da414d2742..8e24dab290a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.16.0 +pyenphase==1.17.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 713c4806606..c7c5e7b0672 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1328,7 +1328,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.16.0 +pyenphase==1.17.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 20b88e30f57af0496e16c473d1a82e8f9457561c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 13 Jan 2024 22:33:02 +0100 Subject: [PATCH 09/29] Update sleep period for Shelly devices with buggy fw (#107961) * update sleep period for Shelly devices with buggy fw * code quality * update model list * add test * Apply review comments * fix test * use costant --- homeassistant/components/shelly/__init__.py | 19 +++++++++++++++++++ homeassistant/components/shelly/const.py | 13 +++++++++++++ tests/components/shelly/test_init.py | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 6b7a00db8e2..6b8d100ea8f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -30,12 +30,15 @@ from homeassistant.helpers.device_registry import ( from homeassistant.helpers.typing import ConfigType from .const import ( + BLOCK_EXPECTED_SLEEP_PERIOD, + BLOCK_WRONG_SLEEP_PERIOD, CONF_COAP_PORT, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DEFAULT_COAP_PORT, DOMAIN, LOGGER, + MODELS_WITH_WRONG_SLEEP_PERIOD, PUSH_UPDATE_ISSUE_ID, ) from .coordinator import ( @@ -162,6 +165,22 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] + # Some old firmware have a wrong sleep period hardcoded value. + # Following code block will force the right value for affected devices + if ( + sleep_period == BLOCK_WRONG_SLEEP_PERIOD + and entry.data["model"] in MODELS_WITH_WRONG_SLEEP_PERIOD + ): + LOGGER.warning( + "Updating stored sleep period for %s: from %s to %s", + entry.title, + sleep_period, + BLOCK_EXPECTED_SLEEP_PERIOD, + ) + data = {**entry.data} + data[CONF_SLEEP_PERIOD] = sleep_period = BLOCK_EXPECTED_SLEEP_PERIOD + hass.config_entries.async_update_entry(entry, data=data) + async def _async_block_device_setup() -> None: """Set up a block based device that is online.""" shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 1e2c22691fb..6cc513015d3 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -14,7 +14,10 @@ from aioshelly.const import ( MODEL_DIMMER, MODEL_DIMMER_2, MODEL_DUO, + MODEL_DW, + MODEL_DW_2, MODEL_GAS, + MODEL_HT, MODEL_MOTION, MODEL_MOTION_2, MODEL_RGBW2, @@ -55,6 +58,12 @@ MODELS_SUPPORTING_LIGHT_EFFECTS: Final = ( MODEL_RGBW2, ) +MODELS_WITH_WRONG_SLEEP_PERIOD: Final = ( + MODEL_DW, + MODEL_DW_2, + MODEL_HT, +) + # Bulbs that support white & color modes DUAL_MODE_LIGHT_MODELS: Final = ( MODEL_BULB, @@ -176,6 +185,10 @@ KELVIN_MAX_VALUE: Final = 6500 KELVIN_MIN_VALUE_WHITE: Final = 2700 KELVIN_MIN_VALUE_COLOR: Final = 3000 +# Sleep period +BLOCK_WRONG_SLEEP_PERIOD = 21600 +BLOCK_EXPECTED_SLEEP_PERIOD = 43200 + UPTIME_DEVIATION: Final = 5 # Time to wait before reloading entry upon device config change diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 643fc775cc4..bc0ba045a55 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -11,8 +11,12 @@ from aioshelly.exceptions import ( import pytest from homeassistant.components.shelly.const import ( + BLOCK_EXPECTED_SLEEP_PERIOD, + BLOCK_WRONG_SLEEP_PERIOD, CONF_BLE_SCANNER_MODE, + CONF_SLEEP_PERIOD, DOMAIN, + MODELS_WITH_WRONG_SLEEP_PERIOD, BLEScannerMode, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -309,3 +313,17 @@ async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device) -> None assert entry.state is ConfigEntryState.LOADED assert hass.states.get("switch.test_name_channel_1").state is STATE_ON + + +@pytest.mark.parametrize(("model"), MODELS_WITH_WRONG_SLEEP_PERIOD) +async def test_sleeping_block_device_wrong_sleep_period( + hass: HomeAssistant, mock_block_device, model +) -> None: + """Test sleeping block device with wrong sleep period.""" + entry = await init_integration( + hass, 1, model=model, sleep_period=BLOCK_WRONG_SLEEP_PERIOD, skip_setup=True + ) + assert entry.data[CONF_SLEEP_PERIOD] == BLOCK_WRONG_SLEEP_PERIOD + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.data[CONF_SLEEP_PERIOD] == BLOCK_EXPECTED_SLEEP_PERIOD From 8a3eb149b7256dd258cbd74140e91823158e5095 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:32:25 -0500 Subject: [PATCH 10/29] Reload ZHA only a single time when the connection is lost multiple times (#107963) * Reload only a single time when the connection is lost multiple times * Ignore when reset task finishes, allow only one reset per `ZHAGateway` --- homeassistant/components/zha/core/gateway.py | 15 ++++++++-- tests/components/zha/test_gateway.py | 30 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 12e439f1059..3efdc77934a 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -142,7 +142,9 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.shutting_down = False + self._reload_task: asyncio.Task | None = None def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" @@ -231,12 +233,17 @@ class ZHAGateway: def connection_lost(self, exc: Exception) -> None: """Handle connection lost event.""" + _LOGGER.debug("Connection to the radio was lost: %r", exc) + if self.shutting_down: return - _LOGGER.debug("Connection to the radio was lost: %r", exc) + # Ensure we do not queue up multiple resets + if self._reload_task is not None: + _LOGGER.debug("Ignoring reset, one is already running") + return - self.hass.async_create_task( + self._reload_task = self.hass.async_create_task( self.hass.config_entries.async_reload(self.config_entry.entry_id) ) @@ -760,6 +767,10 @@ class ZHAGateway: async def shutdown(self) -> None: """Stop ZHA Controller Application.""" + if self.shutting_down: + _LOGGER.debug("Ignoring duplicate shutdown event") + return + _LOGGER.debug("Shutting down ZHA ControllerApplication") self.shutting_down = True diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 4f520920704..9c3cf7aa2f8 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -291,3 +291,33 @@ async def test_gateway_force_multi_pan_channel( _, config = zha_gateway.get_application_controller_data() assert config["network"]["channel"] == expected_channel + + +async def test_single_reload_on_multiple_connection_loss( + hass: HomeAssistant, + zigpy_app_controller: ControllerApplication, + config_entry: MockConfigEntry, +): + """Test that we only reload once when we lose the connection multiple times.""" + config_entry.add_to_hass(hass) + + zha_gateway = ZHAGateway(hass, {}, config_entry) + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): + await zha_gateway.async_initialize() + + with patch.object( + hass.config_entries, "async_reload", wraps=hass.config_entries.async_reload + ) as mock_reload: + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + + assert len(mock_reload.mock_calls) == 1 + + await hass.async_block_till_done() From 507cccdd532e6ab38dbf3fa04bed30d31ab8ae11 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 13 Jan 2024 20:39:34 +0100 Subject: [PATCH 11/29] Don't load entities for docker virtual ethernet interfaces in System Monitor (#107966) --- homeassistant/components/systemmonitor/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index aeb7816784b..75b437c19eb 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -45,6 +45,9 @@ def get_all_network_interfaces() -> set[str]: """Return all network interfaces on system.""" interfaces: set[str] = set() for interface, _ in psutil.net_if_addrs().items(): + if interface.startswith("veth"): + # Don't load docker virtual network interfaces + continue interfaces.add(interface) _LOGGER.debug("Adding interfaces: %s", ", ".join(interfaces)) return interfaces From 9c6f87dd117beff5ed36c556cfbaa4c12ed32dae Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 19 Jan 2024 02:40:36 +1000 Subject: [PATCH 12/29] Improve coordinator logic in Tessie to allow sleep (#107988) * Poll status before state * Tests --- .../components/tessie/binary_sensor.py | 4 +-- homeassistant/components/tessie/const.py | 10 +++++- .../components/tessie/coordinator.py | 22 ++++++++----- tests/components/tessie/common.py | 6 ++-- tests/components/tessie/conftest.py | 16 ++++++++- tests/components/tessie/test_coordinator.py | 33 ++++++++++--------- 6 files changed, 60 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 5edbb108568..e4c0d5d5c66 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, TessieStatus +from .const import DOMAIN, TessieState from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity @@ -30,7 +30,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( TessieBinarySensorEntityDescription( key="state", device_class=BinarySensorDeviceClass.CONNECTIVITY, - is_on=lambda x: x == TessieStatus.ONLINE, + is_on=lambda x: x == TessieState.ONLINE, ), TessieBinarySensorEntityDescription( key="charge_state_battery_heater_on", diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 2ba4e514579..7dea7e65555 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -13,13 +13,21 @@ MODELS = { } -class TessieStatus(StrEnum): +class TessieState(StrEnum): """Tessie status.""" ASLEEP = "asleep" ONLINE = "online" +class TessieStatus(StrEnum): + """Tessie status.""" + + ASLEEP = "asleep" + AWAKE = "awake" + WAITING = "waiting_for_sleep" + + class TessieSeatHeaterOptions(StrEnum): """Tessie seat heater options.""" diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 75cac088bde..c2106af665f 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientResponseError -from tessie_api import get_state +from tessie_api import get_state, get_status from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -45,11 +45,21 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using Tessie API.""" try: + status = await get_status( + session=self.session, + api_key=self.api_key, + vin=self.vin, + ) + if status["status"] == TessieStatus.ASLEEP: + # Vehicle is asleep, no need to poll for data + self.data["state"] = status["status"] + return self.data + vehicle = await get_state( session=self.session, api_key=self.api_key, vin=self.vin, - use_cache=False, + use_cache=True, ) except ClientResponseError as e: if e.status == HTTPStatus.UNAUTHORIZED: @@ -57,13 +67,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from e raise e - if vehicle["state"] == TessieStatus.ONLINE: - # Vehicle is online, all data is fresh - return self._flatten(vehicle) - - # Vehicle is asleep, only update state - self.data["state"] = vehicle["state"] - return self.data + return self._flatten(vehicle) def _flatten( self, data: dict[str, Any], parent: str | None = None diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index ae80526e5d9..ccff7f62b1b 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -6,7 +6,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError from aiohttp.client import RequestInfo -from homeassistant.components.tessie.const import DOMAIN +from homeassistant.components.tessie.const import DOMAIN, TessieStatus from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant @@ -14,7 +14,9 @@ from tests.common import MockConfigEntry, load_json_object_fixture TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) -TEST_VEHICLE_STATE_ASLEEP = load_json_object_fixture("asleep.json", DOMAIN) +TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} +TEST_VEHICLE_STATUS_ASLEEP = {"status": TessieStatus.ASLEEP} + TEST_RESPONSE = {"result": True} TEST_RESPONSE_ERROR = {"result": False, "reason": "reason why"} diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index c7a344d54c5..02b3d56691e 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest -from .common import TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE +from .common import ( + TEST_STATE_OF_ALL_VEHICLES, + TEST_VEHICLE_STATE_ONLINE, + TEST_VEHICLE_STATUS_AWAKE, +) @pytest.fixture @@ -18,6 +22,16 @@ def mock_get_state(): yield mock_get_state +@pytest.fixture +def mock_get_status(): + """Mock get_status function.""" + with patch( + "homeassistant.components.tessie.coordinator.get_status", + return_value=TEST_VEHICLE_STATUS_AWAKE, + ) as mock_get_status: + yield mock_get_status + + @pytest.fixture def mock_get_state_of_all_vehicles(): """Mock get_state_of_all_vehicles function.""" diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 311222466fd..65f91c6f33e 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -10,8 +10,7 @@ from .common import ( ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, - TEST_VEHICLE_STATE_ASLEEP, - TEST_VEHICLE_STATE_ONLINE, + TEST_VEHICLE_STATUS_ASLEEP, setup_platform, ) @@ -20,59 +19,61 @@ from tests.common import async_fire_time_changed WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) -async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_online( + hass: HomeAssistant, mock_get_state, mock_get_status +) -> None: """Tests that the coordinator handles online vehicles.""" - mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() + mock_get_status.assert_called_once() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_ON -async def test_coordinator_asleep(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_asleep(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles asleep vehicles.""" - mock_get_state.return_value = TEST_VEHICLE_STATE_ASLEEP await setup_platform(hass) + mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_OFF -async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles client errors.""" - mock_get_state.side_effect = ERROR_UNKNOWN + mock_get_status.side_effect = ERROR_UNKNOWN await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE -async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_auth(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles timeout errors.""" - mock_get_state.side_effect = ERROR_AUTH + mock_get_status.side_effect = ERROR_AUTH await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() -async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_connection(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles connection errors.""" - mock_get_state.side_effect = ERROR_CONNECTION + mock_get_status.side_effect = ERROR_CONNECTION await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE From ed31adc6dbfe037c6df9a9d820a980308daf50ed Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 15 Jan 2024 20:53:56 +0200 Subject: [PATCH 13/29] Fix Shelly Gen1 entity description restore (#108052) * Fix Shelly Gen1 entity description restore * Update tests/components/shelly/test_sensor.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- .../components/shelly/binary_sensor.py | 13 --------- homeassistant/components/shelly/entity.py | 28 +++++-------------- homeassistant/components/shelly/number.py | 21 +------------- homeassistant/components/shelly/sensor.py | 18 +----------- tests/components/shelly/test_sensor.py | 18 ++++++++++-- 5 files changed, 24 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index b07747f298e..4ad51e5cc0f 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.restore_state import RestoreEntity from .const import CONF_SLEEP_PERIOD @@ -210,16 +209,6 @@ RPC_SENSORS: Final = { } -def _build_block_description(entry: RegistryEntry) -> BlockBinarySensorDescription: - """Build description when restoring block attribute entities.""" - return BlockBinarySensorDescription( - key="", - name="", - icon=entry.original_icon, - device_class=entry.original_device_class, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -248,7 +237,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSleepingBinarySensor, - _build_block_description, ) else: async_setup_entry_attribute_entities( @@ -257,7 +245,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockBinarySensor, - _build_block_description, ) async_setup_entry_rest( hass, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 796402c8bba..3132f1f571e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -39,7 +39,6 @@ def async_setup_entry_attribute_entities( async_add_entities: AddEntitiesCallback, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, - description_class: Callable[[RegistryEntry], BlockEntityDescription], ) -> None: """Set up entities for attributes.""" coordinator = get_entry_data(hass)[config_entry.entry_id].block @@ -56,7 +55,6 @@ def async_setup_entry_attribute_entities( coordinator, sensors, sensor_class, - description_class, ) @@ -113,7 +111,6 @@ def async_restore_block_attribute_entities( coordinator: ShellyBlockCoordinator, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, - description_class: Callable[[RegistryEntry], BlockEntityDescription], ) -> None: """Restore block attributes entities.""" entities = [] @@ -128,11 +125,12 @@ def async_restore_block_attribute_entities( continue attribute = entry.unique_id.split("-")[-1] - description = description_class(entry) + block_type = entry.unique_id.split("-")[-2].split("_")[0] - entities.append( - sensor_class(coordinator, None, attribute, description, entry, sensors) - ) + if description := sensors.get((block_type, attribute)): + entities.append( + sensor_class(coordinator, None, attribute, description, entry) + ) if not entities: return @@ -444,7 +442,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): """Available.""" available = super().available - if not available or not self.entity_description.available: + if not available or not self.entity_description.available or self.block is None: return available return self.entity_description.available(self.block) @@ -559,10 +557,8 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): attribute: str, description: BlockEntityDescription, entry: RegistryEntry | None = None, - sensors: Mapping[tuple[str, str], BlockEntityDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" - self.sensors = sensors self.last_state: State | None = None self.coordinator = coordinator self.attribute = attribute @@ -587,11 +583,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): @callback def _update_callback(self) -> None: """Handle device update.""" - if ( - self.block is not None - or not self.coordinator.device.initialized - or self.sensors is None - ): + if self.block is not None or not self.coordinator.device.initialized: super()._update_callback() return @@ -607,13 +599,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): if sensor_id != entity_sensor: continue - description = self.sensors.get((block.type, sensor_id)) - if description is None: - continue - self.block = block - self.entity_description = description - LOGGER.debug("Entity %s attached to block", self.name) super()._update_callback() return diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 77d066a6106..5d35e71ce5d 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -1,7 +1,6 @@ """Number for Shelly.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Any, Final, cast @@ -56,22 +55,6 @@ NUMBERS: Final = { } -def _build_block_description(entry: RegistryEntry) -> BlockNumberDescription: - """Build description when restoring block attribute entities.""" - assert entry.capabilities - return BlockNumberDescription( - key="", - name="", - icon=entry.original_icon, - native_unit_of_measurement=entry.unit_of_measurement, - device_class=entry.original_device_class, - native_min_value=cast(float, entry.capabilities.get("min")), - native_max_value=cast(float, entry.capabilities.get("max")), - native_step=cast(float, entry.capabilities.get("step")), - mode=cast(NumberMode, entry.capabilities.get("mode")), - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -85,7 +68,6 @@ async def async_setup_entry( async_add_entities, NUMBERS, BlockSleepingNumber, - _build_block_description, ) @@ -101,11 +83,10 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber): attribute: str, description: BlockNumberDescription, entry: RegistryEntry | None = None, - sensors: Mapping[tuple[str, str], BlockNumberDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" self.restored_data: NumberExtraStoredData | None = None - super().__init__(coordinator, block, attribute, description, entry, sensors) + super().__init__(coordinator, block, attribute, description, entry) async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index c7d89f2d284..b439a19e318 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,7 +1,6 @@ """Sensor for Shelly.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Final, cast @@ -36,7 +35,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType -from homeassistant.util.enum import try_parse_enum from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator @@ -963,17 +961,6 @@ RPC_SENSORS: Final = { } -def _build_block_description(entry: RegistryEntry) -> BlockSensorDescription: - """Build description when restoring block attribute entities.""" - return BlockSensorDescription( - key="", - name="", - icon=entry.original_icon, - native_unit_of_measurement=entry.unit_of_measurement, - device_class=try_parse_enum(SensorDeviceClass, entry.original_device_class), - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -1002,7 +989,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSleepingSensor, - _build_block_description, ) else: async_setup_entry_attribute_entities( @@ -1011,7 +997,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSensor, - _build_block_description, ) async_setup_entry_rest( hass, config_entry, async_add_entities, REST_SENSORS, RestSensor @@ -1075,10 +1060,9 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, RestoreSensor): attribute: str, description: BlockSensorDescription, entry: RegistryEntry | None = None, - sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" - super().__init__(coordinator, block, attribute, description, entry, sensors) + super().__init__(coordinator, block, attribute, description, entry) self.restored_data: SensorExtraStoredData | None = None async def async_added_to_hass(self) -> None: diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 380f4f5999e..86c6356191b 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -6,9 +6,15 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, @@ -153,7 +159,11 @@ async def test_block_restored_sleeping_sensor( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == "20.4" + state = hass.states.get(entity_id) + assert state + assert state.state == "20.4" + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) @@ -237,7 +247,9 @@ async def test_block_not_matched_restored_sleeping_sensor( assert hass.states.get(entity_id).state == "20.4" # Make device online - monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "type", "other_type") + monkeypatch.setattr( + mock_block_device.blocks[SENSOR_BLOCK_ID], "description", "other_desc" + ) monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_update() await hass.async_block_till_done() From 70492a80cc0a1eb9a077214af72de482025d5b49 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Mon, 15 Jan 2024 21:41:44 +1300 Subject: [PATCH 14/29] Fix malformed user input error on MJPEG config flow (#108058) --- homeassistant/components/mjpeg/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mjpeg/config_flow.py b/homeassistant/components/mjpeg/config_flow.py index 61c80bcde38..024766f4c63 100644 --- a/homeassistant/components/mjpeg/config_flow.py +++ b/homeassistant/components/mjpeg/config_flow.py @@ -54,7 +54,7 @@ def async_get_schema( if show_name: schema = { - vol.Optional(CONF_NAME, default=defaults.get(CONF_NAME)): str, + vol.Required(CONF_NAME, default=defaults.get(CONF_NAME)): str, **schema, } From 7fee6c5279bf1dc04e1e2d4cf57ec157f7f1c33c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Jan 2024 11:08:38 +0100 Subject: [PATCH 15/29] Fix turning on the light with a specific color (#108080) --- homeassistant/components/matter/light.py | 12 ++++++++++++ tests/components/matter/test_light.py | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 52a6b4162fe..43c47046162 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -89,6 +89,10 @@ class MatterLight(MatterEntity, LightEntity): colorY=int(matter_xy[1]), # It's required in TLV. We don't implement transition time yet. transitionTime=0, + # allow setting the color while the light is off, + # by setting the optionsMask to 1 (=ExecuteIfOff) + optionsMask=1, + optionsOverride=1, ) ) @@ -103,6 +107,10 @@ class MatterLight(MatterEntity, LightEntity): saturation=int(matter_hs[1]), # It's required in TLV. We don't implement transition time yet. transitionTime=0, + # allow setting the color while the light is off, + # by setting the optionsMask to 1 (=ExecuteIfOff) + optionsMask=1, + optionsOverride=1, ) ) @@ -114,6 +122,10 @@ class MatterLight(MatterEntity, LightEntity): colorTemperatureMireds=color_temp, # It's required in TLV. We don't implement transition time yet. transitionTime=0, + # allow setting the color while the light is off, + # by setting the optionsMask to 1 (=ExecuteIfOff) + optionsMask=1, + optionsOverride=1, ) ) diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 78ffa477b33..fb988d26a1c 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -202,6 +202,8 @@ async def test_color_temperature_light( command=clusters.ColorControl.Commands.MoveToColorTemperature( colorTemperatureMireds=300, transitionTime=0, + optionsMask=1, + optionsOverride=1, ), ), call( @@ -278,7 +280,11 @@ async def test_extended_color_light( node_id=light_node.node_id, endpoint_id=1, command=clusters.ColorControl.Commands.MoveToColor( - colorX=0.5 * 65536, colorY=0.5 * 65536, transitionTime=0 + colorX=0.5 * 65536, + colorY=0.5 * 65536, + transitionTime=0, + optionsMask=1, + optionsOverride=1, ), ), call( @@ -311,8 +317,8 @@ async def test_extended_color_light( hue=167, saturation=254, transitionTime=0, - optionsMask=0, - optionsOverride=0, + optionsMask=1, + optionsOverride=1, ), ), call( From 497d2f5677e70f95c041f24f1c06c88d31853a3e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 15 Jan 2024 12:10:17 +0100 Subject: [PATCH 16/29] Bump Jinja2 to 3.1.3 (#108082) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5eebfa4181b..d9df52ea800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 -Jinja2==3.1.2 +Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 orjson==3.9.9 diff --git a/pyproject.toml b/pyproject.toml index 00d8b70f492..33965196ee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ "httpx==0.26.0", "home-assistant-bluetooth==1.12.0", "ifaddr==0.2.0", - "Jinja2==3.1.2", + "Jinja2==3.1.3", "lru-dict==1.3.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. diff --git a/requirements.txt b/requirements.txt index f86893bce46..e1878a33584 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ ciso8601==2.3.0 httpx==0.26.0 home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 -Jinja2==3.1.2 +Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 cryptography==41.0.7 From 99f9f0205a1d31fccc998eae09ec4f116d0321c7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 15 Jan 2024 20:33:30 +0100 Subject: [PATCH 17/29] Use compat for supported features in media player (#108102) --- homeassistant/components/media_player/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 113048421e1..673f0a44374 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1295,7 +1295,7 @@ async def websocket_browse_media( connection.send_error(msg["id"], "entity_not_found", "Entity not found") return - if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features: + if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat: connection.send_message( websocket_api.error_message( msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media" From 304b950f1a2bd9dfbbd1b813e5c90af2f96f8e41 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:36:28 -0500 Subject: [PATCH 18/29] Speed up ZHA initialization and improve startup responsiveness (#108103) * Limit concurrency of startup traffic to allow for interactive usage * Drop `retryable_req`, we already have request retrying * Oops, `min` -> `max` * Add a comment describing why `async_initialize` is not concurrent * Fix existing unit tests * Break out fetching mains state into its own function to unit test --- .../zha/core/cluster_handlers/__init__.py | 3 +- homeassistant/components/zha/core/device.py | 17 ++-- homeassistant/components/zha/core/endpoint.py | 22 ++++- homeassistant/components/zha/core/gateway.py | 49 ++++++++--- homeassistant/components/zha/core/helpers.py | 47 ----------- tests/components/zha/conftest.py | 2 +- tests/components/zha/test_gateway.py | 82 ++++++++++++++++++- 7 files changed, 149 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index 2b78c90aa19..00439343e81 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -42,7 +42,7 @@ from ..const import ( ZHA_CLUSTER_HANDLER_MSG_DATA, ZHA_CLUSTER_HANDLER_READS_PER_REQ, ) -from ..helpers import LogMixin, retryable_req, safe_read +from ..helpers import LogMixin, safe_read if TYPE_CHECKING: from ..endpoint import Endpoint @@ -362,7 +362,6 @@ class ClusterHandler(LogMixin): self.debug("skipping cluster handler configuration") self._status = ClusterHandlerStatus.CONFIGURED - @retryable_req(delays=(1, 1, 3)) async def async_initialize(self, from_cache: bool) -> None: """Initialize cluster handler.""" if not from_cache and self._endpoint.device.skip_configuration: diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1a3d3a2da1f..468e89fbbf0 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -592,12 +592,17 @@ class ZHADevice(LogMixin): self.debug("started initialization") await self._zdo_handler.async_initialize(from_cache) self._zdo_handler.debug("'async_initialize' stage succeeded") - await asyncio.gather( - *( - endpoint.async_initialize(from_cache) - for endpoint in self._endpoints.values() - ) - ) + + # We intentionally do not use `gather` here! This is so that if, for example, + # three `device.async_initialize()`s are spawned, only three concurrent requests + # will ever be in flight at once. Startup concurrency is managed at the device + # level. + for endpoint in self._endpoints.values(): + try: + await endpoint.async_initialize(from_cache) + except Exception: # pylint: disable=broad-exception-caught + self.debug("Failed to initialize endpoint", exc_info=True) + self.debug("power source: %s", self.power_source) self.status = DeviceStatus.INITIALIZED self.debug("completed initialization") diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index 04c253128ee..4dbfccf6f25 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Awaitable, Callable +import functools import logging from typing import TYPE_CHECKING, Any, Final, TypeVar @@ -11,6 +12,7 @@ from zigpy.typing import EndpointType as ZigpyEndpointType from homeassistant.const import Platform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util.async_ import gather_with_limited_concurrency from . import const, discovery, registries from .cluster_handlers import ClusterHandler @@ -169,20 +171,32 @@ class Endpoint: async def async_initialize(self, from_cache: bool = False) -> None: """Initialize claimed cluster handlers.""" - await self._execute_handler_tasks("async_initialize", from_cache) + await self._execute_handler_tasks( + "async_initialize", from_cache, max_concurrency=1 + ) async def async_configure(self) -> None: """Configure claimed cluster handlers.""" await self._execute_handler_tasks("async_configure") - async def _execute_handler_tasks(self, func_name: str, *args: Any) -> None: + async def _execute_handler_tasks( + self, func_name: str, *args: Any, max_concurrency: int | None = None + ) -> None: """Add a throttled cluster handler task and swallow exceptions.""" cluster_handlers = [ *self.claimed_cluster_handlers.values(), *self.client_cluster_handlers.values(), ] tasks = [getattr(ch, func_name)(*args) for ch in cluster_handlers] - results = await asyncio.gather(*tasks, return_exceptions=True) + + gather: Callable[..., Awaitable] + + if max_concurrency is None: + gather = asyncio.gather + else: + gather = functools.partial(gather_with_limited_concurrency, max_concurrency) + + results = await gather(*tasks, return_exceptions=True) for cluster_handler, outcome in zip(cluster_handlers, results): if isinstance(outcome, Exception): cluster_handler.warning( diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3efdc77934a..cca8aa93e99 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -11,7 +11,7 @@ import itertools import logging import re import time -from typing import TYPE_CHECKING, Any, NamedTuple, Self +from typing import TYPE_CHECKING, Any, NamedTuple, Self, cast from zigpy.application import ControllerApplication from zigpy.config import ( @@ -36,6 +36,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import gather_with_limited_concurrency from . import discovery from .const import ( @@ -292,6 +293,39 @@ class ZHAGateway: # entity registry tied to the devices discovery.GROUP_PROBE.discover_group_entities(zha_group) + @property + def radio_concurrency(self) -> int: + """Maximum configured radio concurrency.""" + return self.application_controller._concurrent_requests_semaphore.max_value # pylint: disable=protected-access + + async def async_fetch_updated_state_mains(self) -> None: + """Fetch updated state for mains powered devices.""" + _LOGGER.debug("Fetching current state for mains powered devices") + + now = time.time() + + # Only delay startup to poll mains-powered devices that are online + online_devices = [ + dev + for dev in self.devices.values() + if dev.is_mains_powered + and dev.last_seen is not None + and (now - dev.last_seen) < dev.consider_unavailable_time + ] + + # Prioritize devices that have recently been contacted + online_devices.sort(key=lambda dev: cast(float, dev.last_seen), reverse=True) + + # Make sure that we always leave slots for non-startup requests + max_poll_concurrency = max(1, self.radio_concurrency - 4) + + await gather_with_limited_concurrency( + max_poll_concurrency, + *(dev.async_initialize(from_cache=False) for dev in online_devices), + ) + + _LOGGER.debug("completed fetching current state for mains powered devices") + async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" @@ -302,17 +336,8 @@ class ZHAGateway: async def fetch_updated_state() -> None: """Fetch updated state for mains powered devices.""" - _LOGGER.debug("Fetching current state for mains powered devices") - await asyncio.gather( - *( - dev.async_initialize(from_cache=False) - for dev in self.devices.values() - if dev.is_mains_powered - ) - ) - _LOGGER.debug( - "completed fetching current state for mains powered devices - allowing polled requests" - ) + await self.async_fetch_updated_state_mains() + _LOGGER.debug("Allowing polled requests") self.hass.data[DATA_ZHA].allow_polling = True # background the fetching of state for mains powered devices diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index bb87cb2cf58..72d09d239e1 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -5,17 +5,13 @@ https://home-assistant.io/integrations/zha/ """ from __future__ import annotations -import asyncio import binascii import collections from collections.abc import Callable, Iterator import dataclasses from dataclasses import dataclass import enum -import functools -import itertools import logging -from random import uniform import re from typing import TYPE_CHECKING, Any, TypeVar @@ -318,49 +314,6 @@ class LogMixin: return self.log(logging.ERROR, msg, *args, **kwargs) -def retryable_req( - delays=(1, 5, 10, 15, 30, 60, 120, 180, 360, 600, 900, 1800), raise_=False -): - """Make a method with ZCL requests retryable. - - This adds delays keyword argument to function. - len(delays) is number of tries. - raise_ if the final attempt should raise the exception. - """ - - def decorator(func): - @functools.wraps(func) - async def wrapper(cluster_handler, *args, **kwargs): - exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) - try_count, errors = 1, [] - for delay in itertools.chain(delays, [None]): - try: - return await func(cluster_handler, *args, **kwargs) - except exceptions as ex: - errors.append(ex) - if delay: - delay = uniform(delay * 0.75, delay * 1.25) - cluster_handler.debug( - "%s: retryable request #%d failed: %s. Retrying in %ss", - func.__name__, - try_count, - ex, - round(delay, 1), - ) - try_count += 1 - await asyncio.sleep(delay) - else: - cluster_handler.warning( - "%s: all attempts have failed: %s", func.__name__, errors - ) - if raise_: - raise - - return wrapper - - return decorator - - def convert_install_code(value: str) -> bytes: """Convert string to install code bytes and validate length.""" diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 55405d0a51c..a30c6f35052 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -135,7 +135,7 @@ def _wrap_mock_instance(obj: Any) -> MagicMock: real_attr = getattr(obj, attr_name) mock_attr = getattr(mock, attr_name) - if callable(real_attr): + if callable(real_attr) and not hasattr(real_attr, "__aenter__"): mock_attr.side_effect = real_attr else: setattr(mock, attr_name, real_attr) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 9c3cf7aa2f8..f19ed9bd4a9 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,12 +1,14 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, PropertyMock, patch import pytest from zigpy.application import ControllerApplication import zigpy.profiles.zha as zha +import zigpy.types import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting +import zigpy.zdo.types from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.group import GroupMember @@ -321,3 +323,81 @@ async def test_single_reload_on_multiple_connection_loss( assert len(mock_reload.mock_calls) == 1 await hass.async_block_till_done() + + +@pytest.mark.parametrize("radio_concurrency", [1, 2, 8]) +async def test_startup_concurrency_limit( + radio_concurrency: int, + hass: HomeAssistant, + zigpy_app_controller: ControllerApplication, + config_entry: MockConfigEntry, + zigpy_device_mock, +): + """Test ZHA gateway limits concurrency on startup.""" + config_entry.add_to_hass(hass) + zha_gateway = ZHAGateway(hass, {}, config_entry) + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): + await zha_gateway.async_initialize() + + for i in range(50): + zigpy_dev = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee=f"11:22:33:44:{i:08x}", + nwk=0x1234 + i, + ) + zigpy_dev.node_desc.mac_capability_flags |= ( + zigpy.zdo.types.NodeDescriptor.MACCapabilityFlags.MainsPowered + ) + + zha_gateway._async_get_or_create_device(zigpy_dev, restored=True) + + # Keep track of request concurrency during initialization + current_concurrency = 0 + concurrencies = [] + + async def mock_send_packet(*args, **kwargs): + nonlocal current_concurrency + + current_concurrency += 1 + concurrencies.append(current_concurrency) + + await asyncio.sleep(0.001) + + current_concurrency -= 1 + concurrencies.append(current_concurrency) + + type(zha_gateway).radio_concurrency = PropertyMock(return_value=radio_concurrency) + assert zha_gateway.radio_concurrency == radio_concurrency + + with patch( + "homeassistant.components.zha.core.device.ZHADevice.async_initialize", + side_effect=mock_send_packet, + ): + await zha_gateway.async_fetch_updated_state_mains() + + await zha_gateway.shutdown() + + # Make sure concurrency was always limited + assert current_concurrency == 0 + assert min(concurrencies) == 0 + + if radio_concurrency > 1: + assert 1 <= max(concurrencies) < zha_gateway.radio_concurrency + else: + assert 1 == max(concurrencies) == zha_gateway.radio_concurrency From 7fb2a8a3cd5aea4e3f4210dcb633e80829365cda Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 16 Jan 2024 05:10:09 -0700 Subject: [PATCH 19/29] Bump `aioridwell` to 2024.01.0 (#108126) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 72a29182169..c02cc012e0f 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioridwell"], - "requirements": ["aioridwell==2023.07.0"] + "requirements": ["aioridwell==2024.01.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8e24dab290a..78fcc6f5abb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ aioqsw==0.3.5 aiorecollect==2023.09.0 # homeassistant.components.ridwell -aioridwell==2023.07.0 +aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed aioruckus==0.34 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7c5e7b0672..3841e78796e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aioqsw==0.3.5 aiorecollect==2023.09.0 # homeassistant.components.ridwell -aioridwell==2023.07.0 +aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed aioruckus==0.34 From 5521ab0b35c90121bc4b9d84e25cf988829c6927 Mon Sep 17 00:00:00 2001 From: cnico Date: Tue, 16 Jan 2024 06:56:54 +0100 Subject: [PATCH 20/29] Bump flipr-api to 1.5.1 (#108130) Flipr-api version update for resolution of issue https://github.com/home-assistant/core/issues/105778 --- homeassistant/components/flipr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 73a0b3edb26..898cd640349 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/flipr", "iot_class": "cloud_polling", "loggers": ["flipr_api"], - "requirements": ["flipr-api==1.5.0"] + "requirements": ["flipr-api==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 78fcc6f5abb..d163f46d7d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -840,7 +840,7 @@ fjaraskupan==2.2.0 flexit_bacnet==2.1.0 # homeassistant.components.flipr -flipr-api==1.5.0 +flipr-api==1.5.1 # homeassistant.components.flux_led flux-led==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3841e78796e..d3764c74c58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,7 +675,7 @@ fjaraskupan==2.2.0 flexit_bacnet==2.1.0 # homeassistant.components.flipr -flipr-api==1.5.0 +flipr-api==1.5.1 # homeassistant.components.flux_led flux-led==1.0.4 From d2feee86b7e7c231c7b1ebac13b03e8bc171fe1a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 16 Jan 2024 08:05:35 -0800 Subject: [PATCH 21/29] Add debugging to assist in debugging already configured error (#108134) --- homeassistant/components/google/config_flow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 33d913fe8f1..ed6d36d8ec7 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -210,6 +210,12 @@ class OAuth2FlowHandler( _LOGGER.error("Error reading primary calendar: %s", err) return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(primary_calendar.id) + + if found := self.hass.config_entries.async_entry_for_domain_unique_id( + self.handler, primary_calendar.id + ): + _LOGGER.debug("Found existing '%s' entry: %s", primary_calendar.id, found) + self._abort_if_unique_id_configured() return self.async_create_entry( title=primary_calendar.id, From 901b7b62782f13a301b1f973d94e2e2e1237a09d Mon Sep 17 00:00:00 2001 From: John Allen Date: Wed, 17 Jan 2024 15:06:11 -0500 Subject: [PATCH 22/29] Send target temp to Shelly TRV in F when needed (#108188) --- homeassistant/components/shelly/climate.py | 15 ++++++++++++++ tests/components/shelly/test_climate.py | 23 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 7cc0027bbaf..64129131d0a 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -316,6 +316,21 @@ class BlockSleepingClimate( """Set new target temperature.""" if (current_temp := kwargs.get(ATTR_TEMPERATURE)) is None: return + + # Shelly TRV accepts target_t in Fahrenheit or Celsius, but you must + # send the units that the device expects + if self.block is not None and self.block.channel is not None: + therm = self.coordinator.device.settings["thermostats"][ + int(self.block.channel) + ] + LOGGER.debug("Themostat settings: %s", therm) + if therm.get("target_t", {}).get("units", "C") == "F": + current_temp = TemperatureConverter.convert( + cast(float, current_temp), + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + ) + await self.set_state_full_path(target_t_enabled=1, target_t=f"{current_temp}") async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 980981de754..28235325af4 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -146,6 +146,29 @@ async def test_climate_set_temperature( mock_block_device.http_request.assert_called_once_with( "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "23.0"} ) + mock_block_device.http_request.reset_mock() + + # Test conversion from C to F + monkeypatch.setattr( + mock_block_device, + "settings", + { + "thermostats": [ + {"target_t": {"units": "F"}}, + ] + }, + ) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "68.0"} + ) async def test_climate_set_preset_mode( From da5d4fe4ae15dcdf873f7daa91d2c509e824c145 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 18 Jan 2024 03:34:18 +0100 Subject: [PATCH 23/29] Use cache update for WIFI blinds (#108224) --- homeassistant/components/motion_blinds/coordinator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motion_blinds/coordinator.py b/homeassistant/components/motion_blinds/coordinator.py index cfc7d319b38..e8dc5494f25 100644 --- a/homeassistant/components/motion_blinds/coordinator.py +++ b/homeassistant/components/motion_blinds/coordinator.py @@ -5,7 +5,7 @@ import logging from socket import timeout from typing import Any -from motionblinds import ParseException +from motionblinds import DEVICE_TYPES_WIFI, ParseException from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -59,7 +59,9 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): def update_blind(self, blind): """Fetch data from a blind.""" try: - if self._wait_for_push: + if blind.device_type in DEVICE_TYPES_WIFI: + blind.Update_from_cache() + elif self._wait_for_push: blind.Update() else: blind.Update_trigger() From 6ecb562a800bc7ebbb5593e3fafbf3aa72bff402 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Jan 2024 22:28:15 +0100 Subject: [PATCH 24/29] Bump reolink_aio to 0.8.7 (#108248) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 5670aea87ad..40e85b9680b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.8.6"] + "requirements": ["reolink-aio==0.8.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index d163f46d7d6..80db9c8f26e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2376,7 +2376,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.6 +reolink-aio==0.8.7 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3764c74c58..f7579e24a8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1798,7 +1798,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.6 +reolink-aio==0.8.7 # homeassistant.components.rflink rflink==0.0.65 From 59e12ad0c16c3e29378b6a812dd0a9df616bb860 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 17 Jan 2024 20:54:13 +0100 Subject: [PATCH 25/29] Bump PyTado to 0.17.4 (#108255) Bump to 17.4 --- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index bae637f3180..79fe565261b 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.17.3"] + "requirements": ["python-tado==0.17.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 80db9c8f26e..c1240cd9148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2241,7 +2241,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.17.3 +python-tado==0.17.4 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7579e24a8a..55016596bfb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1696,7 +1696,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.17.3 +python-tado==0.17.4 # homeassistant.components.telegram_bot python-telegram-bot==13.1 From 005af2eb4c60fd5236ba64506cf4220eeb236b9f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 18 Jan 2024 03:33:31 +0100 Subject: [PATCH 26/29] Bump aiounifi to v69 to improve websocket logging (#108265) --- homeassistant/components/unifi/controller.py | 6 +++++- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index a941e836ae2..833d2001980 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -7,6 +7,7 @@ import ssl from types import MappingProxyType from typing import Any, Literal +import aiohttp from aiohttp import CookieJar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -374,7 +375,10 @@ class UniFiController: async def _websocket_runner() -> None: """Start websocket.""" - await self.api.start_websocket() + try: + await self.api.start_websocket() + except (aiohttp.ClientConnectorError, aiounifi.WebsocketError): + LOGGER.error("Websocket disconnected") self.available = False async_dispatcher_send(self.hass, self.signal_reachable) self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 4a43a65d5bb..90b4421f164 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==68"], + "requirements": ["aiounifi==69"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index c1240cd9148..90f0a24453e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -377,7 +377,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==68 +aiounifi==69 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55016596bfb..a7e2fe393cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==68 +aiounifi==69 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From e2a6097141ff337bd093815d411ee60143b18ced Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:34:10 -0500 Subject: [PATCH 27/29] Bump ZHA dependency zigpy to 0.60.6 (#108266) Bump zigpy to 0.60.6 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 06ebfaaa6a0..de429b299c0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.109", "zigpy-deconz==0.22.4", - "zigpy==0.60.4", + "zigpy==0.60.6", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index 90f0a24453e..27bd47f3739 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2887,7 +2887,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.4 +zigpy==0.60.6 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7e2fe393cc..df3eceb14c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2186,7 +2186,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.4 +zigpy==0.60.6 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From 916e5de9d120bda6beb43ec24604d1c493874c51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Jan 2024 15:44:20 +0100 Subject: [PATCH 28/29] Bump version to 2024.1.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9ddb002c261..45f48c4e89e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 33965196ee4..b8b63cf011c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.3" +version = "2024.1.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 43f1c0927fcd169472a51e285ba8335eb28f62a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Jan 2024 19:26:37 +0100 Subject: [PATCH 29/29] Revert "Add debugging to assist in debugging already configured error (#108134)" This reverts commit d2feee86b7e7c231c7b1ebac13b03e8bc171fe1a. --- homeassistant/components/google/config_flow.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index ed6d36d8ec7..33d913fe8f1 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -210,12 +210,6 @@ class OAuth2FlowHandler( _LOGGER.error("Error reading primary calendar: %s", err) return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(primary_calendar.id) - - if found := self.hass.config_entries.async_entry_for_domain_unique_id( - self.handler, primary_calendar.id - ): - _LOGGER.debug("Found existing '%s' entry: %s", primary_calendar.id, found) - self._abort_if_unique_id_configured() return self.async_create_entry( title=primary_calendar.id,