From a2ad4c9cfdf50f661029d52cd7182c482a5bd6a1 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 7 Nov 2024 01:29:44 -0600 Subject: [PATCH 001/123] Bump intents to 2024.11.6 (#129982) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2c446ac5d705..8b5c6ef173ff 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.4"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b399c64d7e2a..c9dbfb6b0623 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ hass-nabucasa==0.83.0 hassil==1.7.4 home-assistant-bluetooth==1.13.0 home-assistant-frontend==20241106.0 -home-assistant-intents==2024.11.4 +home-assistant-intents==2024.11.6 httpx==0.27.2 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 1e50a44c2dd1..c3cfe16dd266 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ holidays==0.60 home-assistant-frontend==20241106.0 # homeassistant.components.conversation -home-assistant-intents==2024.11.4 +home-assistant-intents==2024.11.6 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a04ce2bf63f..e433c005e2ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ holidays==0.60 home-assistant-frontend==20241106.0 # homeassistant.components.conversation -home-assistant-intents==2024.11.4 +home-assistant-intents==2024.11.6 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index f54849ee12b9..9b2079405b94 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.4.28,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.1 \ - PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.1 hassil==1.7.4 home-assistant-intents==2024.11.4 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.1 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 5bd1b0dd9ce98fccafbffe7ea511f584e3fe8ee8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 7 Nov 2024 19:36:43 +1000 Subject: [PATCH 002/123] Fix Trunks in Teslemetry and Tesla Fleet (#129986) --- homeassistant/components/tesla_fleet/cover.py | 8 +------- homeassistant/components/teslemetry/cover.py | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tesla_fleet/cover.py b/homeassistant/components/tesla_fleet/cover.py index 4e49e24b6898..d7e1f68ac895 100644 --- a/homeassistant/components/tesla_fleet/cover.py +++ b/homeassistant/components/tesla_fleet/cover.py @@ -177,13 +177,7 @@ class TeslaFleetRearTrunkEntity(TeslaFleetVehicleEntity, CoverEntity): def _async_update_attrs(self) -> None: """Update the entity attributes.""" - value = self._value - if value == CLOSED: - self._attr_is_closed = True - elif value == OPEN: - self._attr_is_closed = False - else: - self._attr_is_closed = None + self._attr_is_closed = self._value == CLOSED async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index 190f729d99f3..8775da931d59 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -182,13 +182,7 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity): def _async_update_attrs(self) -> None: """Update the entity attributes.""" - value = self._value - if value == CLOSED: - self._attr_is_closed = True - elif value == OPEN: - self._attr_is_closed = False - else: - self._attr_is_closed = None + self._attr_is_closed = self._value == CLOSED async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" From ec7ba1b7fd5778f88fc29702ff24dc84f6bec29a Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 7 Nov 2024 02:14:54 -0500 Subject: [PATCH 003/123] Update sense energy library to 0.13.3 (#129998) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index f1a01f9d7aac..d4889c0c5f5b 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.13.2"] + "requirements": ["sense-energy==0.13.3"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 72d1d045c9a7..df2317c3a6c9 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.13.2"] + "requirements": ["sense-energy==0.13.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c3cfe16dd266..3ccf601ebd33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2623,7 +2623,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.2 +sense-energy==0.13.3 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e433c005e2ba..1abf0f574d0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2090,7 +2090,7 @@ securetar==2024.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.2 +sense-energy==0.13.3 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 1beac5f0f88e819dc5bea742bfd0d3c6d4d44e54 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 7 Nov 2024 01:50:40 -0800 Subject: [PATCH 004/123] Bump google-nest-sdm to 6.1.4 (#130005) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 976e870cc839..581113f0c96a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==6.1.3"] + "requirements": ["google-nest-sdm==6.1.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ccf601ebd33..52b0ab9cf184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1011,7 +1011,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.3 +google-nest-sdm==6.1.4 # homeassistant.components.google_photos google-photos-library-api==0.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1abf0f574d0b..ac8cf51e66a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -861,7 +861,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.3 +google-nest-sdm==6.1.4 # homeassistant.components.google_photos google-photos-library-api==0.12.1 From 2692bc23a5766d80261ef62778456714f4a1480a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:32:23 +0100 Subject: [PATCH 005/123] Add missing placeholder description to twitch (#130013) --- homeassistant/components/twitch/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/twitch/config_flow.py b/homeassistant/components/twitch/config_flow.py index dbaef59c2364..ed196897c113 100644 --- a/homeassistant/components/twitch/config_flow.py +++ b/homeassistant/components/twitch/config_flow.py @@ -78,7 +78,10 @@ class OAuth2FlowHandler( reauth_entry = self._get_reauth_entry() self._abort_if_unique_id_mismatch( reason="wrong_account", - description_placeholders={"title": reauth_entry.title}, + description_placeholders={ + "title": reauth_entry.title, + "username": str(reauth_entry.unique_id), + }, ) new_channels = reauth_entry.options[CONF_CHANNELS] From b0f110b9ab0742bf9e4b64936646c0033b9c0bc0 Mon Sep 17 00:00:00 2001 From: sean t Date: Thu, 7 Nov 2024 17:34:54 +0800 Subject: [PATCH 006/123] Bump agent-py to 0.0.24 (#130018) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/agent_dvr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json index 9a6c528c3366..4ec142963637 100644 --- a/homeassistant/components/agent_dvr/manifest.json +++ b/homeassistant/components/agent_dvr/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/agent_dvr", "iot_class": "local_polling", "loggers": ["agent"], - "requirements": ["agent-py==0.0.23"] + "requirements": ["agent-py==0.0.24"] } diff --git a/requirements_all.txt b/requirements_all.txt index 52b0ab9cf184..b9f2f8aeed29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -152,7 +152,7 @@ advantage-air==0.4.4 afsapi==0.2.7 # homeassistant.components.agent_dvr -agent-py==0.0.23 +agent-py==0.0.24 # homeassistant.components.geo_json_events aio-geojson-generic-client==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac8cf51e66a4..d6d92c778089 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ advantage-air==0.4.4 afsapi==0.2.7 # homeassistant.components.agent_dvr -agent-py==0.0.23 +agent-py==0.0.24 # homeassistant.components.geo_json_events aio-geojson-generic-client==0.4 From 7ff501f3ec8921ee667c890580516b01b245fd1c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Nov 2024 17:07:23 +0100 Subject: [PATCH 007/123] Don't create repairs asking user to remove duplicate ignored config entries (#130056) --- homeassistant/config_entries.py | 11 +++++++++++ tests/test_config_entries.py | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9b5ffcf6fad5..256085682022 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2426,6 +2426,17 @@ class ConfigEntries: for domain, unique_ids in self._entries._domain_unique_id_index.items(): # noqa: SLF001 for unique_id, entries in unique_ids.items(): + # We might mutate the list of entries, so we need a copy to not mess up + # the index + entries = list(entries) + + # There's no need to raise an issue for ignored entries, we can + # safely remove them once we no longer allow unique id collisions. + # Iterate over a copy of the copy to allow mutating while iterating + for entry in list(entries): + if entry.source == SOURCE_IGNORE: + entries.remove(entry) + if len(entries) < 2: continue issue_id = f"{ISSUE_UNIQUE_ID_COLLISION}_{domain}_{unique_id}" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d0a9d5afb4b5..e3594269b580 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7147,6 +7147,12 @@ async def test_unique_id_collision_issues( for _ in range(6): test3.append(MockConfigEntry(domain="test3", unique_id="not_unique")) await manager.async_add(test3[-1]) + # Add an ignored config entry + await manager.async_add( + MockConfigEntry( + domain="test2", unique_id="group_1", source=config_entries.SOURCE_IGNORE + ) + ) # Check we get one issue for domain test2 and one issue for domain test3 assert len(issue_registry.issues) == 2 @@ -7193,7 +7199,7 @@ async def test_unique_id_collision_issues( (HOMEASSISTANT_DOMAIN, "config_entry_unique_id_collision_test2_group_2"), } - # Remove the last test2 group2 duplicate, a new issue is created + # Remove the last test2 group2 duplicate, the issue is cleared await manager.async_remove(test2_group_2[1].entry_id) assert not issue_registry.issues From 5f5f6cc3d54b4aaae0cc2f866c6f6b45c29035b6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 7 Nov 2024 20:03:07 -0800 Subject: [PATCH 008/123] Fix `KeyError` in nest integration when the old key format does not exist (#130057) * Fix bug in nest setup when the old key format does not exist * Further simplify the entry.data check * Update homeassistant/components/nest/api.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/nest/api.py | 5 ++--- tests/components/nest/common.py | 12 ++++++++++++ tests/components/nest/test_init.py | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index aa359dcd1675..5c65a70c75df 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -114,9 +114,8 @@ async def new_subscriber( implementation, config_entry_oauth2_flow.LocalOAuth2Implementation ): raise TypeError(f"Unexpected auth implementation {implementation}") - subscription_name = entry.data.get( - CONF_SUBSCRIPTION_NAME, entry.data[CONF_SUBSCRIBER_ID] - ) + if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None: + subscription_name = entry.data[CONF_SUBSCRIBER_ID] auth = AsyncConfigEntryAuth( aiohttp_client.async_get_clientsession(hass), config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation), diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 9c8de0224f0d..5d4719918a67 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -30,6 +30,7 @@ CLIENT_ID = "some-client-id" CLIENT_SECRET = "some-client-secret" CLOUD_PROJECT_ID = "cloud-id-9876" SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" +SUBSCRIPTION_NAME = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" @dataclass @@ -86,6 +87,17 @@ TEST_CONFIG_ENTRY_LEGACY = NestTestConfig( }, ) +TEST_CONFIG_NEW_SUBSCRIPTION = NestTestConfig( + config_entry_data={ + "sdm": {}, + "project_id": PROJECT_ID, + "cloud_project_id": CLOUD_PROJECT_ID, + "subscription_name": SUBSCRIPTION_NAME, + "auth_implementation": "imported-cred", + }, + credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), +) + class FakeSubscriber(GoogleNestSubscriber): """Fake subscriber that supplies a FakeDeviceManager.""" diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index 4c238683130d..a17803a6cded 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -31,6 +31,7 @@ from .common import ( SUBSCRIBER_ID, TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_LEGACY, + TEST_CONFIG_NEW_SUBSCRIPTION, TEST_CONFIGFLOW_APP_CREDS, FakeSubscriber, PlatformSetup, @@ -97,6 +98,19 @@ async def test_setup_success( assert entries[0].state is ConfigEntryState.LOADED +@pytest.mark.parametrize("nest_test_config", [(TEST_CONFIG_NEW_SUBSCRIPTION)]) +async def test_setup_success_new_subscription_format( + hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform +) -> None: + """Test successful setup.""" + await setup_platform() + assert not error_caplog.records + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + @pytest.mark.parametrize("subscriber_id", [("invalid-subscriber-format")]) async def test_setup_configuration_failure( hass: HomeAssistant, From 9012b113ada14c3b480c1161223546becf6ed4bb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Nov 2024 17:38:10 +0100 Subject: [PATCH 009/123] Don't create repairs asking user to remove duplicate flipr config entries (#130058) * Don't create repairs asking user to remove duplicate flipr config entries * Improve comments --- homeassistant/config_entries.py | 13 +++++++++++- tests/test_config_entries.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 256085682022..2781dea529e4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2147,7 +2147,12 @@ class ConfigEntries: if unique_id is not UNDEFINED and entry.unique_id != unique_id: # Deprecated in 2024.11, should fail in 2025.11 if ( - unique_id is not None + # flipr creates duplicates during migration, and asks users to + # remove the duplicate. We don't need warn about it here too. + # We should remove the special case for "flipr" in HA Core 2025.4, + # when the flipr migration period ends + entry.domain != "flipr" + and unique_id is not None and self.async_entry_for_domain_unique_id(entry.domain, unique_id) is not None ): @@ -2425,6 +2430,12 @@ class ConfigEntries: issues.add(issue.issue_id) for domain, unique_ids in self._entries._domain_unique_id_index.items(): # noqa: SLF001 + # flipr creates duplicates during migration, and asks users to + # remove the duplicate. We don't need warn about it here too. + # We should remove the special case for "flipr" in HA Core 2025.4, + # when the flipr migration period ends + if domain == "flipr": + continue for unique_id, entries in unique_ids.items(): # We might mutate the list of entries, so we need a copy to not mess up # the index diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e3594269b580..d530628d27cb 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7118,6 +7118,41 @@ async def test_async_update_entry_unique_id_collision( assert issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) +@pytest.mark.parametrize("domain", ["flipr"]) +async def test_async_update_entry_unique_id_collision_allowed_domain( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, + issue_registry: ir.IssueRegistry, + domain: str, +) -> None: + """Test we warn when async_update_entry creates a unique_id collision. + + This tests we don't warn and don't create issues for domains which have + their own migration path. + """ + assert len(issue_registry.issues) == 0 + + entry1 = MockConfigEntry(domain=domain, unique_id=None) + entry2 = MockConfigEntry(domain=domain, unique_id="not none") + entry3 = MockConfigEntry(domain=domain, unique_id="very unique") + entry4 = MockConfigEntry(domain=domain, unique_id="also very unique") + entry1.add_to_manager(manager) + entry2.add_to_manager(manager) + entry3.add_to_manager(manager) + entry4.add_to_manager(manager) + + manager.async_update_entry(entry2, unique_id=None) + assert len(issue_registry.issues) == 0 + assert len(caplog.record_tuples) == 0 + + manager.async_update_entry(entry4, unique_id="very unique") + assert len(issue_registry.issues) == 0 + assert len(caplog.record_tuples) == 0 + + assert ("already in use") not in caplog.text + + async def test_unique_id_collision_issues( hass: HomeAssistant, manager: config_entries.ConfigEntries, From 29620ef97775d8e65c0fe67ec94a92429bde6c91 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 8 Nov 2024 08:47:28 +0100 Subject: [PATCH 010/123] Add missing string to tedee plus test (#130081) --- homeassistant/components/tedee/strings.json | 3 +- tests/components/tedee/test_config_flow.py | 37 +++++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tedee/strings.json b/homeassistant/components/tedee/strings.json index 2dc0e23968c8..b6966fa2933d 100644 --- a/homeassistant/components/tedee/strings.json +++ b/homeassistant/components/tedee/strings.json @@ -38,7 +38,8 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "unique_id_mismatch": "You selected a different bridge than the one this config entry was configured with, this is not allowed." }, "error": { "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index d3654783bd6d..2e86286c8da2 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -7,10 +7,11 @@ from pytedee_async import ( TedeeDataUpdateException, TedeeLocalAuthException, ) +from pytedee_async.bridge import TedeeBridge import pytest from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -134,11 +135,10 @@ async def test_reauth_flow( assert result["reason"] == "reauth_successful" -async def test_reconfigure_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock -) -> None: - """Test that the reconfigure flow works.""" - +async def __do_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> ConfigFlowResult: + """Initialize a reconfigure flow.""" mock_config_entry.add_to_hass(hass) reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) @@ -146,11 +146,19 @@ async def test_reconfigure_flow( assert reconfigure_result["type"] is FlowResultType.FORM assert reconfigure_result["step_id"] == "reconfigure" - result = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( reconfigure_result["flow_id"], {CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, CONF_HOST: "192.168.1.43"}, ) + +async def test_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock +) -> None: + """Test that the reconfigure flow works.""" + + result = await __do_reconfigure_flow(hass, mock_config_entry) + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -162,3 +170,18 @@ async def test_reconfigure_flow( CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, CONF_WEBHOOK_ID: WEBHOOK_ID, } + + +async def test_reconfigure_unique_id_mismatch( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock +) -> None: + """Ensure reconfigure flow aborts when the bride changes.""" + + mock_tedee.get_local_bridge.return_value = TedeeBridge( + 0, "1111-1111", "Bridge-R2D2" + ) + + result = await __do_reconfigure_flow(hass, mock_config_entry) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" From 0206c149cff8191f2294e0ed47232e4169f58817 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 8 Nov 2024 16:15:57 +0100 Subject: [PATCH 011/123] Force int value on port in P1Monitor (#130084) --- homeassistant/components/p1_monitor/config_flow.py | 11 +++++++---- tests/components/p1_monitor/test_config_flow.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/p1_monitor/config_flow.py b/homeassistant/components/p1_monitor/config_flow.py index 055973e8e37c..a7ede186d727 100644 --- a/homeassistant/components/p1_monitor/config_flow.py +++ b/homeassistant/components/p1_monitor/config_flow.py @@ -57,10 +57,13 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema( { vol.Required(CONF_HOST): TextSelector(), - vol.Required(CONF_PORT, default=80): NumberSelector( - NumberSelectorConfig( - mode=NumberSelectorMode.BOX, - ) + vol.Required(CONF_PORT, default=80): vol.All( + NumberSelector( + NumberSelectorConfig( + min=1, max=65535, mode=NumberSelectorMode.BOX + ), + ), + vol.Coerce(int), ), } ), diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index ea1d12055a0e..cbd89320074a 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -36,6 +36,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "P1 Monitor" assert result2.get("data") == {CONF_HOST: "example.com", CONF_PORT: 80} + assert isinstance(result2["data"][CONF_PORT], int) assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_p1monitor.mock_calls) == 1 From 46e37f3bddae98b02ff6e2de077f9613ea044f03 Mon Sep 17 00:00:00 2001 From: Kelvin Dekker <143089625+KelvinDekker@users.noreply.github.com> Date: Fri, 8 Nov 2024 08:47:02 +0100 Subject: [PATCH 012/123] Fix typo in insteon strings (#130085) --- homeassistant/components/insteon/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index 1464a2dbc8f4..4df997ac9393 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -112,7 +112,7 @@ "services": { "add_all_link": { "name": "Add all link", - "description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.", + "description": "Tells the Insteon Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.", "fields": { "group": { "name": "Group", From 09d03e8edf56b9d950f244477d1fe31686a7cf1d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Nov 2024 08:46:48 +0100 Subject: [PATCH 013/123] Update frontend to 20241106.1 (#130086) --- 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 2df14df4523b..1ac7e661abe5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20241106.0"] + "requirements": ["home-assistant-frontend==20241106.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c9dbfb6b0623..2ea099278f63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ habluetooth==3.6.0 hass-nabucasa==0.83.0 hassil==1.7.4 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20241106.0 +home-assistant-frontend==20241106.1 home-assistant-intents==2024.11.6 httpx==0.27.2 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b9f2f8aeed29..fc2379151cbe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1124,7 +1124,7 @@ hole==0.8.0 holidays==0.60 # homeassistant.components.frontend -home-assistant-frontend==20241106.0 +home-assistant-frontend==20241106.1 # homeassistant.components.conversation home-assistant-intents==2024.11.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6d92c778089..ca33ca67b0b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ hole==0.8.0 holidays==0.60 # homeassistant.components.frontend -home-assistant-frontend==20241106.0 +home-assistant-frontend==20241106.1 # homeassistant.components.conversation home-assistant-intents==2024.11.6 From 35539dbf607289e26f12be29bb474bff408637bb Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Fri, 8 Nov 2024 02:39:41 -0500 Subject: [PATCH 014/123] Bump python-roborock to 2.7.2 (#130100) --- homeassistant/components/roborock/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- .../roborock/snapshots/test_diagnostics.ambr | 16 ++++++++++++++++ 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 79a9bf77578f..c305e4710fce 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.6.1", + "python-roborock==2.7.2", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2ea099278f63..e22996d50bdb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -166,7 +166,7 @@ get-mac==1000000000.0.0 charset-normalizer==3.2.0 # dacite: Ensure we have a version that is able to handle type unions for -# Roborock, NAM, Brother, and GIOS. +# NAM, Brother, and GIOS. dacite>=1.7.0 # Musle wheels for pandas 2.2.0 cannot be build for any architecture. diff --git a/requirements_all.txt b/requirements_all.txt index fc2379151cbe..8c5d5fea9a46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2393,7 +2393,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.6.1 +python-roborock==2.7.2 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca33ca67b0b5..3bf837da6e12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1914,7 +1914,7 @@ python-picnic-api==1.1.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.6.1 +python-roborock==2.7.2 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 36962ce1fe94..8730acb38670 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -182,7 +182,7 @@ get-mac==1000000000.0.0 charset-normalizer==3.2.0 # dacite: Ensure we have a version that is able to handle type unions for -# Roborock, NAM, Brother, and GIOS. +# NAM, Brother, and GIOS. dacite>=1.7.0 # Musle wheels for pandas 2.2.0 cannot be build for any architecture. diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 805a498041a6..26ecb729312e 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -102,6 +102,7 @@ 'id': '120', 'mode': 'ro', 'name': '错误代码', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -109,6 +110,7 @@ 'id': '121', 'mode': 'ro', 'name': '设备状态', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -116,6 +118,7 @@ 'id': '122', 'mode': 'ro', 'name': '设备电量', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -123,6 +126,7 @@ 'id': '123', 'mode': 'rw', 'name': '清扫模式', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -130,6 +134,7 @@ 'id': '124', 'mode': 'rw', 'name': '拖地模式', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -137,6 +142,7 @@ 'id': '125', 'mode': 'rw', 'name': '主刷寿命', + 'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', 'type': 'VALUE', }), dict({ @@ -144,6 +150,7 @@ 'id': '126', 'mode': 'rw', 'name': '边刷寿命', + 'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', 'type': 'VALUE', }), dict({ @@ -151,6 +158,7 @@ 'id': '127', 'mode': 'rw', 'name': '滤网寿命', + 'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', 'type': 'VALUE', }), dict({ @@ -381,6 +389,7 @@ 'id': '120', 'mode': 'ro', 'name': '错误代码', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -388,6 +397,7 @@ 'id': '121', 'mode': 'ro', 'name': '设备状态', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -395,6 +405,7 @@ 'id': '122', 'mode': 'ro', 'name': '设备电量', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -402,6 +413,7 @@ 'id': '123', 'mode': 'rw', 'name': '清扫模式', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -409,6 +421,7 @@ 'id': '124', 'mode': 'rw', 'name': '拖地模式', + 'property': '{"range": []}', 'type': 'ENUM', }), dict({ @@ -416,6 +429,7 @@ 'id': '125', 'mode': 'rw', 'name': '主刷寿命', + 'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', 'type': 'VALUE', }), dict({ @@ -423,6 +437,7 @@ 'id': '126', 'mode': 'rw', 'name': '边刷寿命', + 'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', 'type': 'VALUE', }), dict({ @@ -430,6 +445,7 @@ 'id': '127', 'mode': 'rw', 'name': '滤网寿命', + 'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', 'type': 'VALUE', }), dict({ From b0b163df48fd7f0940a95a80401dc9e29344357a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Nov 2024 15:13:05 +0100 Subject: [PATCH 015/123] Update frontend to 20241106.2 (#130128) --- 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 1ac7e661abe5..4dc5a2b0ae47 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20241106.1"] + "requirements": ["home-assistant-frontend==20241106.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e22996d50bdb..a7fde36ac07d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ habluetooth==3.6.0 hass-nabucasa==0.83.0 hassil==1.7.4 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20241106.1 +home-assistant-frontend==20241106.2 home-assistant-intents==2024.11.6 httpx==0.27.2 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8c5d5fea9a46..63cc58558785 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1124,7 +1124,7 @@ hole==0.8.0 holidays==0.60 # homeassistant.components.frontend -home-assistant-frontend==20241106.1 +home-assistant-frontend==20241106.2 # homeassistant.components.conversation home-assistant-intents==2024.11.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf837da6e12..9b41300408c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ hole==0.8.0 holidays==0.60 # homeassistant.components.frontend -home-assistant-frontend==20241106.1 +home-assistant-frontend==20241106.2 # homeassistant.components.conversation home-assistant-intents==2024.11.6 From b71383c997dbc92fdda15ae23bc83c1dd8a17df2 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Fri, 8 Nov 2024 17:15:28 +0200 Subject: [PATCH 016/123] Fix issue when timestamp is None (#130133) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/seventeentrack/services.py | 33 +++++++++------- .../snapshots/test_services.ambr | 29 ++++++++++++++ .../seventeentrack/test_services.py | 38 +++++++++++++++++++ 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/seventeentrack/services.py b/homeassistant/components/seventeentrack/services.py index 0833bc0a97bc..54c23e6d6191 100644 --- a/homeassistant/components/seventeentrack/services.py +++ b/homeassistant/components/seventeentrack/services.py @@ -1,8 +1,8 @@ """Services for the seventeentrack integration.""" -from typing import Final +from typing import Any, Final -from pyseventeentrack.package import PACKAGE_STATUS_MAP +from pyseventeentrack.package import PACKAGE_STATUS_MAP, Package import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -81,18 +81,7 @@ def setup_services(hass: HomeAssistant) -> None: return { "packages": [ - { - ATTR_DESTINATION_COUNTRY: package.destination_country, - ATTR_ORIGIN_COUNTRY: package.origin_country, - ATTR_PACKAGE_TYPE: package.package_type, - ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language, - ATTR_TRACKING_NUMBER: package.tracking_number, - ATTR_LOCATION: package.location, - ATTR_STATUS: package.status, - ATTR_TIMESTAMP: package.timestamp.isoformat(), - ATTR_INFO_TEXT: package.info_text, - ATTR_FRIENDLY_NAME: package.friendly_name, - } + package_to_dict(package) for package in live_packages if slugify(package.status) in package_states or package_states == [] ] @@ -110,6 +99,22 @@ def setup_services(hass: HomeAssistant) -> None: await seventeen_coordinator.client.profile.archive_package(tracking_number) + def package_to_dict(package: Package) -> dict[str, Any]: + result = { + ATTR_DESTINATION_COUNTRY: package.destination_country, + ATTR_ORIGIN_COUNTRY: package.origin_country, + ATTR_PACKAGE_TYPE: package.package_type, + ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language, + ATTR_TRACKING_NUMBER: package.tracking_number, + ATTR_LOCATION: package.location, + ATTR_STATUS: package.status, + ATTR_INFO_TEXT: package.info_text, + ATTR_FRIENDLY_NAME: package.friendly_name, + } + if timestamp := package.timestamp: + result[ATTR_TIMESTAMP] = timestamp.isoformat() + return result + async def _validate_service(config_entry_id): entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id) if not entry: diff --git a/tests/components/seventeentrack/snapshots/test_services.ambr b/tests/components/seventeentrack/snapshots/test_services.ambr index 568acea33a5f..e172a2de5947 100644 --- a/tests/components/seventeentrack/snapshots/test_services.ambr +++ b/tests/components/seventeentrack/snapshots/test_services.ambr @@ -71,3 +71,32 @@ ]), }) # --- +# name: test_packages_with_none_timestamp + dict({ + 'packages': list([ + dict({ + 'destination_country': 'Belgium', + 'friendly_name': 'friendly name 1', + 'info_text': 'info text 1', + 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', + 'status': 'In Transit', + 'tracking_info_language': 'Unknown', + 'tracking_number': '456', + }), + dict({ + 'destination_country': 'Belgium', + 'friendly_name': 'friendly name 2', + 'info_text': 'info text 1', + 'location': 'location 1', + 'origin_country': 'Belgium', + 'package_type': 'Registered Parcel', + 'status': 'Delivered', + 'timestamp': '2020-08-10T10:32:00+00:00', + 'tracking_info_language': 'Unknown', + 'tracking_number': '789', + }), + ]), + }) +# --- diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py index 54c9349c121f..bbd5644ad63c 100644 --- a/tests/components/seventeentrack/test_services.py +++ b/tests/components/seventeentrack/test_services.py @@ -150,6 +150,28 @@ async def test_archive_package( ) +async def test_packages_with_none_timestamp( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure service returns all packages when non provided.""" + await _mock_invalid_packages(mock_seventeentrack) + await init_integration(hass, mock_config_entry) + service_response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + CONFIG_ENTRY_ID_KEY: mock_config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + + assert service_response == snapshot + + async def _mock_packages(mock_seventeentrack): package1 = get_package(status=10) package2 = get_package( @@ -167,3 +189,19 @@ async def _mock_packages(mock_seventeentrack): package2, package3, ] + + +async def _mock_invalid_packages(mock_seventeentrack): + package1 = get_package( + status=10, + timestamp=None, + ) + package2 = get_package( + tracking_number="789", + friendly_name="friendly name 2", + status=40, + ) + mock_seventeentrack.return_value.profile.packages.return_value = [ + package1, + package2, + ] From 22822cb8aa3a368b3aa37d476685d7d77db38f56 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 8 Nov 2024 15:54:14 +0100 Subject: [PATCH 017/123] Add go2rtc workaround for HA managed one until upstream fixes it (#130139) --- homeassistant/components/go2rtc/__init__.py | 75 +++++-- homeassistant/components/go2rtc/const.py | 1 + homeassistant/components/go2rtc/server.py | 15 +- tests/components/go2rtc/test_init.py | 211 ++++++++++++++++++-- tests/components/go2rtc/test_server.py | 5 +- 5 files changed, 270 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index a07a62305f2e..ca4aeeed9384 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -1,5 +1,8 @@ """The go2rtc component.""" +from __future__ import annotations + +from dataclasses import dataclass import logging import shutil @@ -38,7 +41,13 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from homeassistant.util.package import is_docker_env -from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL +from .const import ( + CONF_DEBUG_UI, + DEBUG_UI_URL_MESSAGE, + DOMAIN, + HA_MANAGED_RTSP_PORT, + HA_MANAGED_URL, +) from .server import Server _LOGGER = logging.getLogger(__name__) @@ -85,13 +94,22 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN) +_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN) _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) +@dataclass(frozen=True) +class Go2RtcData: + """Data for go2rtc.""" + + url: str + managed: bool + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up WebRTC.""" url: str | None = None + managed = False if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config: await _remove_go2rtc_entries(hass) return True @@ -126,8 +144,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) url = HA_MANAGED_URL + managed = True - hass.data[_DATA_GO2RTC] = url + hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed) discovery_flow.async_create_flow( hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} ) @@ -142,28 +161,32 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up go2rtc from a config entry.""" - url = hass.data[_DATA_GO2RTC] + data = hass.data[_DATA_GO2RTC] # Validate the server URL try: - client = Go2RtcRestClient(async_get_clientsession(hass), url) + client = Go2RtcRestClient(async_get_clientsession(hass), data.url) await client.validate_server_version() except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( - f"Could not connect to go2rtc instance on {url}" + f"Could not connect to go2rtc instance on {data.url}" ) from err - _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) + _LOGGER.warning( + "Could not connect to go2rtc instance on %s (%s)", data.url, err + ) return False except Go2RtcVersionError as err: raise ConfigEntryNotReady( f"The go2rtc server version is not supported, {err}" ) from err except Exception as err: # noqa: BLE001 - _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) + _LOGGER.warning( + "Could not connect to go2rtc instance on %s (%s)", data.url, err + ) return False - provider = WebRTCProvider(hass, url) + provider = WebRTCProvider(hass, data) async_register_webrtc_provider(hass, provider) return True @@ -181,12 +204,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None: class WebRTCProvider(CameraWebRTCProvider): """WebRTC provider.""" - def __init__(self, hass: HomeAssistant, url: str) -> None: + def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None: """Initialize the WebRTC provider.""" self._hass = hass - self._url = url + self._data = data self._session = async_get_clientsession(hass) - self._rest_client = Go2RtcRestClient(self._session, url) + self._rest_client = Go2RtcRestClient(self._session, data.url) self._sessions: dict[str, Go2RtcWsClient] = {} @property @@ -208,7 +231,7 @@ class WebRTCProvider(CameraWebRTCProvider): ) -> None: """Handle the WebRTC offer and return the answer via the provided callback.""" self._sessions[session_id] = ws_client = Go2RtcWsClient( - self._session, self._url, source=camera.entity_id + self._session, self._data.url, source=camera.entity_id ) if not (stream_source := await camera.stream_source()): @@ -219,8 +242,30 @@ class WebRTCProvider(CameraWebRTCProvider): streams = await self._rest_client.streams.list() - if (stream := streams.get(camera.entity_id)) is None or not any( - stream_source == producer.url for producer in stream.producers + if self._data.managed: + # HA manages the go2rtc instance + stream_org_name = camera.entity_id + "_orginal" + stream_redirect_sources = [ + f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_org_name}", + f"ffmpeg:{stream_org_name}#audio=opus", + ] + + if ( + (stream_org := streams.get(stream_org_name)) is None + or not any( + stream_source == producer.url for producer in stream_org.producers + ) + or (stream_redirect := streams.get(camera.entity_id)) is None + or stream_redirect_sources != [p.url for p in stream_redirect.producers] + ): + await self._rest_client.streams.add(stream_org_name, stream_source) + await self._rest_client.streams.add( + camera.entity_id, stream_redirect_sources + ) + + # go2rtc instance is managed outside HA + elif (stream_org := streams.get(camera.entity_id)) is None or not any( + stream_source == producer.url for producer in stream_org.producers ): await self._rest_client.streams.add( camera.entity_id, diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index d33ae3e38975..3c4dc9a95009 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,3 +6,4 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" +HA_MANAGED_RTSP_PORT = 18554 diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index ed3b44aadf9a..91f4433546ca 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL +from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL _LOGGER = logging.getLogger(__name__) _TERMINATE_TIMEOUT = 5 @@ -24,15 +24,16 @@ _RESPAWN_COOLDOWN = 1 # Default configuration for HA # - Api is listening only on localhost -# - Disable rtsp listener +# - Enable rtsp for localhost only as ffmpeg needs it # - Clear default ice servers -_GO2RTC_CONFIG_FORMAT = r""" +_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant +# Do not edit it manually + api: listen: "{api_ip}:{api_port}" rtsp: - # ffmpeg needs rtsp for opus audio transcoding - listen: "127.0.0.1:18554" + listen: "127.0.0.1:{rtsp_port}" webrtc: listen: ":18555/tcp" @@ -67,7 +68,9 @@ def _create_temp_file(api_ip: str) -> str: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: file.write( _GO2RTC_CONFIG_FORMAT.format( - api_ip=api_ip, api_port=HA_MANAGED_API_PORT + api_ip=api_ip, + api_port=HA_MANAGED_API_PORT, + rtsp_port=HA_MANAGED_RTSP_PORT, ).encode() ) return file.name diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 18a46fdd4d1c..ea1971a31d9e 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Generator import logging from typing import NamedTuple -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, call, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from go2rtc_client import Stream @@ -296,7 +296,7 @@ async def _test_setup_and_signaling( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_go_binary( +async def test_setup_managed( hass: HomeAssistant, rest_client: AsyncMock, ws_client: Mock, @@ -308,15 +308,131 @@ async def test_setup_go_binary( config: ConfigType, ui_enabled: bool, ) -> None: - """Test the go2rtc config entry with binary.""" + """Test the go2rtc setup with managed go2rtc instance.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry + camera = init_test_integration - def after_setup() -> None: - server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) - server_start.assert_called_once() + entity_id = camera.entity_id + stream_name_orginal = camera.entity_id + "_orginal" + assert camera.frontend_stream_type == StreamType.HLS - await _test_setup_and_signaling( - hass, rest_client, ws_client, config, after_setup, init_test_integration + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].state == ConfigEntryState.LOADED + server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) + server_start.assert_called_once() + + receive_message_callback = Mock(spec_set=WebRTCSendMessage) + + async def test() -> None: + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + ws_client.send.assert_called_once_with( + WebRTCOffer( + OFFER_SDP, + camera.async_get_webrtc_client_configuration().configuration.ice_servers, + ) + ) + ws_client.subscribe.assert_called_once() + + # Simulate the answer from the go2rtc server + callback = ws_client.subscribe.call_args[0][0] + callback(WebRTCAnswer(ANSWER_SDP)) + receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) + + await test() + + stream_added_calls = [ + call(stream_name_orginal, "rtsp://stream"), + call( + entity_id, + [ + f"rtsp://127.0.0.1:18554/{stream_name_orginal}", + f"ffmpeg:{stream_name_orginal}#audio=opus", + ], + ), + ] + assert rest_client.streams.add.call_args_list == stream_added_calls + + # Stream original missing + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + entity_id: Stream( + [ + Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"), + Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"), + ] + ) + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + assert rest_client.streams.add.call_args_list == stream_added_calls + + # Stream original source different + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + stream_name_orginal: Stream([Producer("rtsp://different")]), + entity_id: Stream( + [ + Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"), + Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"), + ] + ), + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + assert rest_client.streams.add.call_args_list == stream_added_calls + + # Stream source different + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + stream_name_orginal: Stream([Producer("rtsp://stream")]), + entity_id: Stream([Producer("rtsp://different")]), + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + assert rest_client.streams.add.call_args_list == stream_added_calls + + # If the stream is already added, the stream should not be added again. + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + stream_name_orginal: Stream([Producer("rtsp://stream")]), + entity_id: Stream( + [ + Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"), + Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"), + ] + ), + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + rest_client.streams.add.assert_not_called() + assert isinstance(camera._webrtc_provider, WebRTCProvider) + + # Set stream source to None and provider should be skipped + rest_client.streams.list.return_value = {} + receive_message_callback.reset_mock() + camera.set_stream_source(None) + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + receive_message_callback.assert_called_once_with( + WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") ) await hass.async_stop() @@ -332,7 +448,7 @@ async def test_setup_go_binary( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_go( +async def test_setup_self_hosted( hass: HomeAssistant, rest_client: AsyncMock, ws_client: Mock, @@ -342,16 +458,83 @@ async def test_setup_go( mock_is_docker_env: Mock, has_go2rtc_entry: bool, ) -> None: - """Test the go2rtc config entry without binary.""" + """Test the go2rtc with selfhosted go2rtc instance.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}} + camera = init_test_integration - def after_setup() -> None: - server.assert_not_called() + entity_id = camera.entity_id + assert camera.frontend_stream_type == StreamType.HLS - await _test_setup_and_signaling( - hass, rest_client, ws_client, config, after_setup, init_test_integration + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].state == ConfigEntryState.LOADED + server.assert_not_called() + + receive_message_callback = Mock(spec_set=WebRTCSendMessage) + + async def test() -> None: + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + ws_client.send.assert_called_once_with( + WebRTCOffer( + OFFER_SDP, + camera.async_get_webrtc_client_configuration().configuration.ice_servers, + ) + ) + ws_client.subscribe.assert_called_once() + + # Simulate the answer from the go2rtc server + callback = ws_client.subscribe.call_args[0][0] + callback(WebRTCAnswer(ANSWER_SDP)) + receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) + + await test() + + rest_client.streams.add.assert_called_once_with( + entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + ) + + # Stream exists but the source is different + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + entity_id: Stream([Producer("rtsp://different")]) + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + rest_client.streams.add.assert_called_once_with( + entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + ) + + # If the stream is already added, the stream should not be added again. + rest_client.streams.add.reset_mock() + rest_client.streams.list.return_value = { + entity_id: Stream([Producer("rtsp://stream")]) + } + + receive_message_callback.reset_mock() + ws_client.reset_mock() + await test() + + rest_client.streams.add.assert_not_called() + assert isinstance(camera._webrtc_provider, WebRTCProvider) + + # Set stream source to None and provider should be skipped + rest_client.streams.list.return_value = {} + receive_message_callback.reset_mock() + camera.set_stream_source(None) + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + receive_message_callback.assert_called_once_with( + WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") ) mock_get_binary.assert_not_called() diff --git a/tests/components/go2rtc/test_server.py b/tests/components/go2rtc/test_server.py index d810dbd88eb9..e4fe3993f3cd 100644 --- a/tests/components/go2rtc/test_server.py +++ b/tests/components/go2rtc/test_server.py @@ -105,12 +105,13 @@ async def test_server_run_success( # Verify that the config file was written mock_tempfile.write.assert_called_once_with( - f""" + f"""# This file is managed by Home Assistant +# Do not edit it manually + api: listen: "{api_ip}:11984" rtsp: - # ffmpeg needs rtsp for opus audio transcoding listen: "127.0.0.1:18554" webrtc: From aa2c3b046f36126776a0cb0c632cf70fcaf816c8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 8 Nov 2024 15:53:26 +0100 Subject: [PATCH 018/123] Bump spotifyaio to 0.8.7 (#130140) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 8cf8d7355538..afe352904ceb 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -9,6 +9,6 @@ "iot_class": "cloud_polling", "loggers": ["spotipy"], "quality_scale": "silver", - "requirements": ["spotifyaio==0.8.5"], + "requirements": ["spotifyaio==0.8.7"], "zeroconf": ["_spotify-connect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 63cc58558785..7c89cba75218 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2707,7 +2707,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.5 +spotifyaio==0.8.7 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b41300408c1..eebe53a410d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2159,7 +2159,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.5 +spotifyaio==0.8.7 # homeassistant.components.sql sqlparse==0.5.0 From 2fe4fc908bbe1c3c6559a079ab103c1d9bab8d36 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 8 Nov 2024 16:00:24 +0100 Subject: [PATCH 019/123] Bump ha-ffmpeg to 3.2.2 (#130142) --- homeassistant/components/ffmpeg/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index e5f4f8b93a82..085db6791b33 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -4,5 +4,5 @@ "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/ffmpeg", "integration_type": "system", - "requirements": ["ha-ffmpeg==3.2.1"] + "requirements": ["ha-ffmpeg==3.2.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a7fde36ac07d..793dbfddd962 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.24.3 fnv-hash-fast==1.0.2 go2rtc-client==0.1.0 ha-av==10.1.1 -ha-ffmpeg==3.2.1 +ha-ffmpeg==3.2.2 habluetooth==3.6.0 hass-nabucasa==0.83.0 hassil==1.7.4 diff --git a/requirements_all.txt b/requirements_all.txt index 7c89cba75218..6b1c00a5e22b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1069,7 +1069,7 @@ h2==4.1.0 ha-av==10.1.1 # homeassistant.components.ffmpeg -ha-ffmpeg==3.2.1 +ha-ffmpeg==3.2.2 # homeassistant.components.iotawatt ha-iotawattpy==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eebe53a410d6..698d91f4120e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -907,7 +907,7 @@ h2==4.1.0 ha-av==10.1.1 # homeassistant.components.ffmpeg -ha-ffmpeg==3.2.1 +ha-ffmpeg==3.2.2 # homeassistant.components.iotawatt ha-iotawattpy==0.1.2 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 9b2079405b94..26db1fe5a1a5 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.4.28,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.1 \ - PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.1 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 1bb0ced7c02bfb25dab1260d4ccb397ec5eb07f1 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Fri, 8 Nov 2024 18:40:43 +0100 Subject: [PATCH 020/123] Fix volume_up not working in some cases in bluesound integration (#130146) --- .../components/bluesound/media_player.py | 2 +- .../components/bluesound/test_media_player.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 1d46af2cc4b5..97985a74300c 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -770,7 +770,7 @@ class BluesoundPlayer(MediaPlayerEntity): async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" - volume = int(volume * 100) + volume = int(round(volume * 100)) volume = min(100, volume) volume = max(0, volume) diff --git a/tests/components/bluesound/test_media_player.py b/tests/components/bluesound/test_media_player.py index 894528265e1b..0bf615de3da8 100644 --- a/tests/components/bluesound/test_media_player.py +++ b/tests/components/bluesound/test_media_player.py @@ -345,3 +345,31 @@ async def test_attr_bluesound_group( ).attributes.get("bluesound_group") assert attr_bluesound_group == ["player-name1111", "player-name2222"] + + +async def test_volume_up_from_6_to_7( + hass: HomeAssistant, + setup_config_entry: None, + player_mocks: PlayerMocks, +) -> None: + """Test the media player volume up from 6 to 7. + + This fails if if rounding is not done correctly. See https://github.com/home-assistant/core/issues/129956 for more details. + """ + player_mocks.player_data.status_long_polling_mock.set( + dataclasses.replace( + player_mocks.player_data.status_long_polling_mock.get(), volume=6 + ) + ) + + # give the long polling loop a chance to update the state; this could be any async call + await hass.async_block_till_done() + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_VOLUME_UP, + {ATTR_ENTITY_ID: "media_player.player_name1111"}, + blocking=True, + ) + + player_mocks.player_data.player.volume.assert_called_once_with(level=7) From 864b4d86f28e0399116860b60a8ffcb861e45313 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Nov 2024 09:37:00 -0800 Subject: [PATCH 021/123] Fix bugs in nest stream expiration handling (#130150) --- homeassistant/components/nest/camera.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 30f96f819c19..2bee54df3dd7 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -235,7 +235,9 @@ class NestWebRTCEntity(NestCameraBaseEntity): async def _async_refresh_stream(self) -> None: """Refresh stream to extend expiration time.""" now = utcnow() - for webrtc_stream in list(self._webrtc_sessions.values()): + for session_id, webrtc_stream in list(self._webrtc_sessions.items()): + if session_id not in self._webrtc_sessions: + continue if now < (webrtc_stream.expires_at - STREAM_EXPIRATION_BUFFER): _LOGGER.debug( "Stream does not yet expire: %s", webrtc_stream.expires_at @@ -247,7 +249,8 @@ class NestWebRTCEntity(NestCameraBaseEntity): except ApiException as err: _LOGGER.debug("Failed to extend stream: %s", err) else: - self._webrtc_sessions[webrtc_stream.media_session_id] = webrtc_stream + if session_id in self._webrtc_sessions: + self._webrtc_sessions[session_id] = webrtc_stream async def async_camera_image( self, width: int | None = None, height: int | None = None From da8fc7a2fcc0b7cca85fde11206453f53293cca4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Nov 2024 18:36:19 +0100 Subject: [PATCH 022/123] Refrase imap fetch service description string (#130152) --- homeassistant/components/imap/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 115d46f3d0e4..7c4a0d9a9736 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -104,7 +104,7 @@ "services": { "fetch": { "name": "Fetch message", - "description": "Fetch the email message from the server.", + "description": "Fetch an email message from the server.", "fields": { "entry": { "name": "Entry", From c16fb9c93d15615bbfba9175c4f3a85fd7e55223 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Nov 2024 18:58:21 +0100 Subject: [PATCH 023/123] Bump version to 2024.11.1 --- 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 2988834d3b0d..210e1cfedfeb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 11 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 6b21d117d9cb..729235e9766b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.11.0" +version = "2024.11.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cc457938968932a8e3c2dadd3e7948d1c23ebf36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Nov 2024 02:42:20 -0600 Subject: [PATCH 024/123] Bump aiohttp to 3.10.11 (#130483) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- .../advantage_air/test_binary_sensor.py | 44 ++++++------------- tests/components/advantage_air/test_sensor.py | 24 +++------- tests/components/generic/test_camera.py | 4 +- 6 files changed, 26 insertions(+), 52 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 793dbfddd962..5b7e1b4e837f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.10 +aiohttp==3.10.11 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 729235e9766b..8699a1a627f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.10.10", + "aiohttp==3.10.11", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 73c674fbc321..67b875943d7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.10.10 +aiohttp==3.10.11 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 13bbadb38f98..d0088d96ba56 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -1,10 +1,8 @@ """Test the Advantage Air Binary Sensor Platform.""" from datetime import timedelta -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch -from homeassistant.components.advantage_air import ADVANTAGE_AIR_SYNC_INTERVAL -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -70,22 +68,14 @@ async def test_binary_sensor_async_setup_entry( assert not hass.states.get(entity_id) mock_get.reset_mock() - entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1), - ) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(mock_get.mock_calls) == 1 + with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1): + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(mock_get.mock_calls) == 2 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state @@ -101,22 +91,14 @@ async def test_binary_sensor_async_setup_entry( assert not hass.states.get(entity_id) mock_get.reset_mock() - entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1), - ) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(mock_get.mock_calls) == 1 + with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1): + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(mock_get.mock_calls) == 2 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 06243921a645..3ea368a59fbd 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -1,15 +1,13 @@ """Test the Advantage Air Sensor Platform.""" from datetime import timedelta -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch -from homeassistant.components.advantage_air import ADVANTAGE_AIR_SYNC_INTERVAL from homeassistant.components.advantage_air.const import DOMAIN as ADVANTAGE_AIR_DOMAIN from homeassistant.components.advantage_air.sensor import ( ADVANTAGE_AIR_SERVICE_SET_TIME_TO, ADVANTAGE_AIR_SET_COUNTDOWN_VALUE, ) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -124,23 +122,15 @@ async def test_sensor_platform_disabled_entity( assert not hass.states.get(entity_id) - entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) - await hass.async_block_till_done(wait_background_tasks=True) mock_get.reset_mock() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1), - ) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(mock_get.mock_calls) == 1 + with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1): + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done(wait_background_tasks=True) - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(mock_get.mock_calls) == 2 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 59ff513ccc9f..d3ef0a39241c 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -275,7 +275,9 @@ async def test_limit_refetch( with ( pytest.raises(aiohttp.ServerTimeoutError), - patch("asyncio.timeout", side_effect=TimeoutError()), + patch.object( + client.session._connector, "connect", side_effect=asyncio.TimeoutError + ), ): resp = await client.get("/api/camera_proxy/camera.config_test") From f040060b3c303ffe2693926e6cb00aae971c27f1 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:54:37 +0100 Subject: [PATCH 025/123] Fix RecursionError in Husqvarna Automower coordinator (#123085) * reach maximum recursion depth exceeded in tests * second background task * Update homeassistant/components/husqvarna_automower/coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/husqvarna_automower/coordinator.py Co-authored-by: Martin Hjelmare * test * modify test * tests * use correct exception * reset mock * use recursion_limit * remove unneeded ticks * test TimeoutException * set lower recursionlimit * remove not that important comment and move the other * test that we connect and listen successfully * Simulate hass shutting down * skip testing against the recursion limit * Update homeassistant/components/husqvarna_automower/coordinator.py Co-authored-by: Martin Hjelmare * mock * Remove comment * Revert "mock" This reverts commit e8ddaea3d79ed1aceb696a055cc42ad08b4febca. * Move patch to decorator * Make execution of patched methods predictable * Parametrize test, make mocked start_listening block * Apply suggestions from code review --------- Co-authored-by: Martin Hjelmare Co-authored-by: Erik --- .../husqvarna_automower/coordinator.py | 30 ++++--- .../husqvarna_automower/conftest.py | 8 ++ .../husqvarna_automower/test_init.py | 81 +++++++++++++++---- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 458ff50dac9d..c19f37a040d9 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -8,6 +8,7 @@ from aioautomower.exceptions import ( ApiException, AuthException, HusqvarnaWSServerHandshakeError, + TimeoutException, ) from aioautomower.model import MowerAttributes from aioautomower.session import AutomowerSession @@ -22,6 +23,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) MAX_WS_RECONNECT_TIME = 600 SCAN_INTERVAL = timedelta(minutes=8) +DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]): @@ -40,8 +42,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib update_interval=SCAN_INTERVAL, ) self.api = api - self.ws_connected: bool = False + self.reconnect_time = DEFAULT_RECONNECT_TIME async def _async_update_data(self) -> dict[str, MowerAttributes]: """Subscribe for websocket and poll data from the API.""" @@ -66,24 +68,28 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib hass: HomeAssistant, entry: ConfigEntry, automower_client: AutomowerSession, - reconnect_time: int = 2, ) -> None: """Listen with the client.""" try: await automower_client.auth.websocket_connect() - reconnect_time = 2 + # Reset reconnect time after successful connection + self.reconnect_time = DEFAULT_RECONNECT_TIME await automower_client.start_listening() except HusqvarnaWSServerHandshakeError as err: _LOGGER.debug( - "Failed to connect to websocket. Trying to reconnect: %s", err + "Failed to connect to websocket. Trying to reconnect: %s", + err, + ) + except TimeoutException as err: + _LOGGER.debug( + "Failed to listen to websocket. Trying to reconnect: %s", + err, ) - if not hass.is_stopping: - await asyncio.sleep(reconnect_time) - reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME) - await self.client_listen( - hass=hass, - entry=entry, - automower_client=automower_client, - reconnect_time=reconnect_time, + await asyncio.sleep(self.reconnect_time) + self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME) + entry.async_create_background_task( + hass, + self.client_listen(hass, entry, automower_client), + "reconnect_task", ) diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 2814e1558d13..0202cec05b97 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Husqvarna Automower.""" +import asyncio from collections.abc import Generator import time from unittest.mock import AsyncMock, patch @@ -101,10 +102,17 @@ async def setup_credentials(hass: HomeAssistant) -> None: def mock_automower_client(values) -> Generator[AsyncMock]: """Mock a Husqvarna Automower client.""" + async def listen() -> None: + """Mock listen.""" + listen_block = asyncio.Event() + await listen_block.wait() + pytest.fail("Listen was not cancelled!") + mock = AsyncMock(spec=AutomowerSession) mock.auth = AsyncMock(side_effect=ClientWebSocketResponse) mock.commands = AsyncMock(spec_set=_MowerCommands) mock.get_status.return_value = values + mock.start_listening = AsyncMock(side_effect=listen) with patch( "homeassistant.components.husqvarna_automower.AutomowerSession", diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index b2127145372a..acf10d33004a 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -1,14 +1,16 @@ """Tests for init module.""" -from datetime import datetime, timedelta +from asyncio import Event +from datetime import datetime import http import time -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ( ApiException, AuthException, HusqvarnaWSServerHandshakeError, + TimeoutException, ) from aioautomower.model import MowerAttributes, WorkArea from freezegun.api import FrozenDateTimeFactory @@ -127,28 +129,77 @@ async def test_update_failed( assert entry.state is entry_state +@patch( + "homeassistant.components.husqvarna_automower.coordinator.DEFAULT_RECONNECT_TIME", 0 +) +@pytest.mark.parametrize( + ("method_path", "exception", "error_msg"), + [ + ( + ["auth", "websocket_connect"], + HusqvarnaWSServerHandshakeError, + "Failed to connect to websocket.", + ), + ( + ["start_listening"], + TimeoutException, + "Failed to listen to websocket.", + ), + ], +) async def test_websocket_not_available( hass: HomeAssistant, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, freezer: FrozenDateTimeFactory, + method_path: list[str], + exception: type[Exception], + error_msg: str, ) -> None: - """Test trying reload the websocket.""" - mock_automower_client.start_listening.side_effect = HusqvarnaWSServerHandshakeError( - "Boom" - ) + """Test trying to reload the websocket.""" + calls = [] + mock_called = Event() + mock_stall = Event() + + async def mock_function(): + mock_called.set() + await mock_stall.wait() + # Raise the first time the method is awaited + if not calls: + calls.append(None) + raise exception("Boom") + if mock_side_effect: + await mock_side_effect() + + # Find the method to mock + mock = mock_automower_client + for itm in method_path: + mock = getattr(mock, itm) + mock_side_effect = mock.side_effect + mock.side_effect = mock_function + + # Setup integration and verify log error message await setup_integration(hass, mock_config_entry) - assert "Failed to connect to websocket. Trying to reconnect: Boom" in caplog.text - assert mock_automower_client.auth.websocket_connect.call_count == 1 - assert mock_automower_client.start_listening.call_count == 1 - assert mock_config_entry.state is ConfigEntryState.LOADED - freezer.tick(timedelta(seconds=2)) - async_fire_time_changed(hass) + await mock_called.wait() + mock_called.clear() + # Allow the exception to be raised + mock_stall.set() + assert mock.call_count == 1 await hass.async_block_till_done() - assert mock_automower_client.auth.websocket_connect.call_count == 2 - assert mock_automower_client.start_listening.call_count == 2 - assert mock_config_entry.state is ConfigEntryState.LOADED + assert f"{error_msg} Trying to reconnect: Boom" in caplog.text + + # Simulate a successful connection + caplog.clear() + await mock_called.wait() + mock_called.clear() + await hass.async_block_till_done() + assert mock.call_count == 2 + assert "Trying to reconnect: Boom" not in caplog.text + + # Simulate hass shutting down + await hass.async_stop() + assert mock.call_count == 2 async def test_device_info( From b27e0f9fe7db3596a8539842e0355969caac85af Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:14:33 +0100 Subject: [PATCH 026/123] Bump python-linkplay to v0.0.18 (#130159) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index f2b2e2da00c1..9ddb6abf0939 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.0.17"], + "requirements": ["python-linkplay==0.0.18"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6b1c00a5e22b..903f6b888d69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2356,7 +2356,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.7 # homeassistant.components.linkplay -python-linkplay==0.0.17 +python-linkplay==0.0.18 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 698d91f4120e..08eede3b7711 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1883,7 +1883,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.7 # homeassistant.components.linkplay -python-linkplay==0.0.17 +python-linkplay==0.0.18 # homeassistant.components.matter python-matter-server==6.6.0 From 32dc9fc2381010a6e876eec048633b4eeee337c1 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:02:55 +0100 Subject: [PATCH 027/123] Allow dynamic max preset in linkplay play preset (#130160) --- homeassistant/components/linkplay/media_player.py | 5 ++++- homeassistant/components/linkplay/services.yaml | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 36834610c04a..71971d3e558f 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -292,7 +292,10 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): @exception_wrap async def async_play_preset(self, preset_number: int) -> None: """Play preset number.""" - await self._bridge.player.play_preset(preset_number) + try: + await self._bridge.player.play_preset(preset_number) + except ValueError as err: + raise HomeAssistantError(err) from err @exception_wrap async def async_join_players(self, group_members: list[str]) -> None: diff --git a/homeassistant/components/linkplay/services.yaml b/homeassistant/components/linkplay/services.yaml index 20bc47be7a77..0d7335a28c85 100644 --- a/homeassistant/components/linkplay/services.yaml +++ b/homeassistant/components/linkplay/services.yaml @@ -11,5 +11,4 @@ play_preset: selector: number: min: 1 - max: 10 mode: box From f914642e315f0ec7027e056815e4ebd730faa464 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:11:34 +0100 Subject: [PATCH 028/123] No longer thrown an error when device is offline in linkplay (#130161) --- homeassistant/components/linkplay/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 71971d3e558f..a625412852ee 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -9,7 +9,7 @@ from typing import Any, Concatenate from linkplay.bridge import LinkPlayBridge from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus from linkplay.controller import LinkPlayController, LinkPlayMultiroom -from linkplay.exceptions import LinkPlayException, LinkPlayRequestException +from linkplay.exceptions import LinkPlayRequestException import voluptuous as vol from homeassistant.components import media_source @@ -201,9 +201,8 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): try: await self._bridge.player.update_status() self._update_properties() - except LinkPlayException: + except LinkPlayRequestException: self._attr_available = False - raise @exception_wrap async def async_select_source(self, source: str) -> None: From 701a901fe4d541f218da0a6918fb5dc30231dfe6 Mon Sep 17 00:00:00 2001 From: Sheldon Ip <4224778+sheldonip@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:15:17 -0800 Subject: [PATCH 029/123] Fix translations in ollama (#130164) --- homeassistant/components/ollama/strings.json | 4 +++- tests/components/ollama/test_config_flow.py | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ollama/strings.json b/homeassistant/components/ollama/strings.json index c307f160228d..248cac34f115 100644 --- a/homeassistant/components/ollama/strings.json +++ b/homeassistant/components/ollama/strings.json @@ -11,9 +11,11 @@ "title": "Downloading model" } }, + "abort": { + "download_failed": "Model downloading failed" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "download_failed": "Model downloading failed", "unknown": "[%key:common::config_flow::error::unknown%]" }, "progress": { diff --git a/tests/components/ollama/test_config_flow.py b/tests/components/ollama/test_config_flow.py index 82c954a1737c..7755f2208b43 100644 --- a/tests/components/ollama/test_config_flow.py +++ b/tests/components/ollama/test_config_flow.py @@ -204,10 +204,6 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None: assert result2["errors"] == {"base": error} -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.ollama.config.abort.download_failed"], -) async def test_download_error(hass: HomeAssistant) -> None: """Test we handle errors while downloading a model.""" result = await hass.config_entries.flow.async_init( From 7f4f90f06d6efb729290cc792f0e255c30532cc8 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:12:14 -0500 Subject: [PATCH 030/123] Bump nice-go to 0.3.10 (#130173) Bump Nice G.O. to 0.3.10 --- homeassistant/components/nice_go/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nice_go/manifest.json b/homeassistant/components/nice_go/manifest.json index d3f54e5e6684..817d7ef9bc9d 100644 --- a/homeassistant/components/nice_go/manifest.json +++ b/homeassistant/components/nice_go/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["nice_go"], - "requirements": ["nice-go==0.3.9"] + "requirements": ["nice-go==0.3.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 903f6b888d69..8d602b535b35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1454,7 +1454,7 @@ nextdns==3.3.0 nibe==2.11.0 # homeassistant.components.nice_go -nice-go==0.3.9 +nice-go==0.3.10 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08eede3b7711..36bbfbb10140 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1214,7 +1214,7 @@ nextdns==3.3.0 nibe==2.11.0 # homeassistant.components.nice_go -nice-go==0.3.9 +nice-go==0.3.10 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From 9579e4a9c1b6d4b54749d1b9cdadef39678c1853 Mon Sep 17 00:00:00 2001 From: Max Shcherbina <17325179+maxshcherbina@users.noreply.github.com> Date: Sat, 9 Nov 2024 07:01:59 -0500 Subject: [PATCH 031/123] Fix wording in Google Calendar create_event strings for consistency (#130183) --- homeassistant/components/google/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index c029b46051e7..2ea45239a530 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -87,8 +87,8 @@ } }, "create_event": { - "name": "Creates event", - "description": "Add a new calendar event.", + "name": "Create event", + "description": "Adds a new calendar event.", "fields": { "summary": { "name": "Summary", From 6b91c0810a5e0a60e74df648b369d360e4ee84bf Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 9 Nov 2024 15:55:39 +0100 Subject: [PATCH 032/123] Fix uptime sensor for Vodafone Station (#130215) --- homeassistant/components/vodafone_station/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index 136aa94b43af..fb76253eb3d1 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -43,12 +43,10 @@ def _calculate_uptime( ) -> datetime: """Calculate device uptime.""" - assert isinstance(last_value, datetime) - delta_uptime = coordinator.api.convert_uptime(coordinator.data.sensors[key]) if ( - not last_value + not isinstance(last_value, datetime) or abs((delta_uptime - last_value).total_seconds()) > UPTIME_DEVIATION ): return delta_uptime From 592b8ed0a08769fb59257eebdb5f56d9fc4494cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 9 Nov 2024 15:54:58 +0100 Subject: [PATCH 033/123] Bump pyTibber (#130216) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 205bc1352ebd..d1bfefec4848 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.30.4"] + "requirements": ["pyTibber==0.30.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8d602b535b35..8fda0c111beb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.30.4 +pyTibber==0.30.7 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36bbfbb10140..0183fcdb2533 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ pyElectra==1.2.4 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.30.4 +pyTibber==0.30.7 # homeassistant.components.dlink pyW215==0.7.0 From 4ea9574229f19c49492846529ea0cfb9df19c282 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 9 Nov 2024 10:14:40 -0600 Subject: [PATCH 034/123] Bump SoCo to 0.30.6 (#130223) --- 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 d6c5eb298d82..76a7d0bfa919 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco"], - "requirements": ["soco==0.30.4", "sonos-websocket==0.1.3"], + "requirements": ["soco==0.30.6", "sonos-websocket==0.1.3"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index 8fda0c111beb..5b094d114822 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2683,7 +2683,7 @@ smhi-pkg==1.0.18 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.4 +soco==0.30.6 # homeassistant.components.solaredge_local solaredge-local==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0183fcdb2533..ab5069155bd4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2138,7 +2138,7 @@ smhi-pkg==1.0.18 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.4 +soco==0.30.6 # homeassistant.components.solarlog solarlog_cli==0.3.2 From c399d8f5711ff2e2d4c922c950bc8ec22f5529f1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 9 Nov 2024 10:02:15 -0800 Subject: [PATCH 035/123] Bump google-nest-sdm to 6.1.5 (#130229) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 581113f0c96a..44eaeeaf62d1 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==6.1.4"] + "requirements": ["google-nest-sdm==6.1.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b094d114822..a7992c184308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1011,7 +1011,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.4 +google-nest-sdm==6.1.5 # homeassistant.components.google_photos google-photos-library-api==0.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab5069155bd4..0cd3b94af4f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -861,7 +861,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.4 +google-nest-sdm==6.1.5 # homeassistant.components.google_photos google-photos-library-api==0.12.1 From 9f447af468e3e5749780ff9a728af62a08a805da Mon Sep 17 00:00:00 2001 From: Olivier Corradi <1655848+corradio@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:34:29 +0100 Subject: [PATCH 036/123] Rename "CO2 Signal" display name to Electricity Maps for consistency (#130242) * Update strings.json for Electricity Maps * Update strings.json * Update config_flow.py * Update test_config_flow.py * Fix test --- homeassistant/components/co2signal/config_flow.py | 2 +- tests/components/co2signal/test_config_flow.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 622c09f0d382..0d357cce1993 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -168,7 +168,7 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=get_extra_name(data) or "CO2 Signal", + title=get_extra_name(data) or "Electricity Maps", data=data, ) diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 92d9450b6703..f8f94d441264 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -44,7 +44,7 @@ async def test_form_home(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "CO2 Signal" + assert result2["title"] == "Electricity Maps" assert result2["data"] == { "api_key": "api_key", } @@ -185,7 +185,7 @@ async def test_form_error_handling( await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "CO2 Signal" + assert result["title"] == "Electricity Maps" assert result["data"] == { "api_key": "api_key", } From 07a8cf14cd67bc0b1f8270474180bb25f4c56341 Mon Sep 17 00:00:00 2001 From: Max Shcherbina <17325179+maxshcherbina@users.noreply.github.com> Date: Sun, 10 Nov 2024 16:13:38 -0500 Subject: [PATCH 037/123] Update generic thermostat strings for clarity and accuracy (#130243) --- homeassistant/components/generic_thermostat/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json index 1ddd41de7344..51549dc844e2 100644 --- a/homeassistant/components/generic_thermostat/strings.json +++ b/homeassistant/components/generic_thermostat/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "title": "Add generic thermostat helper", + "title": "Add generic thermostat", "description": "Create a climate entity that controls the temperature via a switch and sensor.", "data": { "ac_mode": "Cooling mode", @@ -17,8 +17,8 @@ "data_description": { "ac_mode": "Set the actuator specified to be treated as a cooling device instead of a heating device.", "heater": "Switch entity used to cool or heat depending on A/C mode.", - "target_sensor": "Temperature sensor that reflect the current temperature.", - "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on. This option will be ignored if the keep alive option is set.", + "target_sensor": "Temperature sensor that reflects the current temperature.", + "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.", "cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.", "hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5." } From 83baa1a788a7f9439e1106076513b7c5ac03a8f5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 10 Nov 2024 16:28:58 +0100 Subject: [PATCH 038/123] Fix translation key for `done` response in conversation (#130247) --- .../components/conversation/default_agent.py | 2 +- .../conversation/test_default_agent.py | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 6b5cef89fd66..a7110c357955 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -294,7 +294,7 @@ class DefaultAgent(ConversationEntity): self.hass, language, DOMAIN, [DOMAIN] ) response_text = translations.get( - f"component.{DOMAIN}.agent.done", "Done" + f"component.{DOMAIN}.conversation.agent.done", "Done" ) response.async_set_speech(response_text) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 14a9b0ca88c5..9f54671d8a1b 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -418,6 +418,44 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None: assert len(callback.mock_calls) == 0 +@pytest.mark.parametrize( + ("language", "expected"), + [("en", "English done"), ("de", "German done"), ("not_translated", "Done")], +) +@pytest.mark.usefixtures("init_components") +async def test_trigger_sentence_response_translation( + hass: HomeAssistant, language: str, expected: str +) -> None: + """Test translation of default response 'done'.""" + hass.config.language = language + + agent = hass.data[DATA_DEFAULT_ENTITY] + assert isinstance(agent, default_agent.DefaultAgent) + + translations = { + "en": {"component.conversation.conversation.agent.done": "English done"}, + "de": {"component.conversation.conversation.agent.done": "German done"}, + "not_translated": {}, + } + + with patch( + "homeassistant.components.conversation.default_agent.translation.async_get_translations", + return_value=translations.get(language), + ): + unregister = agent.register_trigger( + ["test sentence"], AsyncMock(return_value=None) + ) + result = await conversation.async_converse( + hass, "test sentence", None, Context() + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.speech == { + "plain": {"speech": expected, "extra_data": None} + } + + unregister() + + @pytest.mark.usefixtures("init_components", "sl_setup") async def test_shopping_list_add_item(hass: HomeAssistant) -> None: """Test adding an item to the shopping list through the default agent.""" From d408b7ac6295e3b1a00f7b7cca9f487382d3fc1a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Nov 2024 03:01:59 -0800 Subject: [PATCH 039/123] Improve nest camera stream expiration to be defensive against errors (#130265) --- homeassistant/components/nest/camera.py | 176 ++++++++++++++---------- tests/components/nest/test_camera.py | 44 ++++++ 2 files changed, 144 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 2bee54df3dd7..4cb88e636416 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -2,9 +2,9 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import ABC import asyncio -from collections.abc import Callable +from collections.abc import Awaitable, Callable import datetime import functools import logging @@ -46,6 +46,11 @@ PLACEHOLDER = Path(__file__).parent / "placeholder.png" # Used to schedule an alarm to refresh the stream before expiration STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) +# Refresh streams with a bounded interval and backoff on failure +MIN_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=1) +MAX_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=10) +BACKOFF_MULTIPLIER = 1.5 + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -67,6 +72,68 @@ async def async_setup_entry( async_add_entities(entities) +class StreamRefresh: + """Class that will refresh an expiring stream. + + This class will schedule an alarm for the next expiration time of a stream. + When the alarm fires, it runs the provided `refresh_cb` to extend the + lifetime of the stream and return a new expiration time. + + A simple backoff will be applied when the refresh callback fails. + """ + + def __init__( + self, + hass: HomeAssistant, + expires_at: datetime.datetime, + refresh_cb: Callable[[], Awaitable[datetime.datetime | None]], + ) -> None: + """Initialize StreamRefresh.""" + self._hass = hass + self._unsub: Callable[[], None] | None = None + self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL + self._refresh_cb = refresh_cb + self._schedule_stream_refresh(expires_at - STREAM_EXPIRATION_BUFFER) + + def unsub(self) -> None: + """Invalidates the stream.""" + if self._unsub: + self._unsub() + + async def _handle_refresh(self, _: datetime.datetime) -> None: + """Alarm that fires to check if the stream should be refreshed.""" + self._unsub = None + try: + expires_at = await self._refresh_cb() + except ApiException as err: + _LOGGER.debug("Failed to refresh stream: %s", err) + # Increase backoff until the max backoff interval is reached + self._min_refresh_interval = min( + self._min_refresh_interval * BACKOFF_MULTIPLIER, + MAX_REFRESH_BACKOFF_INTERVAL, + ) + refresh_time = utcnow() + self._min_refresh_interval + else: + if expires_at is None: + return + self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL # Reset backoff + # Defend against invalid stream expiration time in the past + refresh_time = max( + expires_at - STREAM_EXPIRATION_BUFFER, + utcnow() + self._min_refresh_interval, + ) + self._schedule_stream_refresh(refresh_time) + + def _schedule_stream_refresh(self, refresh_time: datetime.datetime) -> None: + """Schedules an alarm to refresh any streams before expiration.""" + _LOGGER.debug("Scheduling stream refresh for %s", refresh_time) + self._unsub = async_track_point_in_utc_time( + self._hass, + self._handle_refresh, + refresh_time, + ) + + class NestCameraBaseEntity(Camera, ABC): """Devices that support cameras.""" @@ -86,41 +153,6 @@ class NestCameraBaseEntity(Camera, ABC): self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 # The API "name" field is a unique device identifier. self._attr_unique_id = f"{self._device.name}-camera" - self._stream_refresh_unsub: Callable[[], None] | None = None - - @abstractmethod - def _stream_expires_at(self) -> datetime.datetime | None: - """Next time when a stream expires.""" - - @abstractmethod - async def _async_refresh_stream(self) -> None: - """Refresh any stream to extend expiration time.""" - - def _schedule_stream_refresh(self) -> None: - """Schedules an alarm to refresh any streams before expiration.""" - if self._stream_refresh_unsub is not None: - self._stream_refresh_unsub() - - expiration_time = self._stream_expires_at() - if not expiration_time: - return - refresh_time = expiration_time - STREAM_EXPIRATION_BUFFER - _LOGGER.debug("Scheduled next stream refresh for %s", refresh_time) - - self._stream_refresh_unsub = async_track_point_in_utc_time( - self.hass, - self._handle_stream_refresh, - refresh_time, - ) - - async def _handle_stream_refresh(self, _: datetime.datetime) -> None: - """Alarm that fires to check if the stream should be refreshed.""" - _LOGGER.debug("Examining streams to refresh") - self._stream_refresh_unsub = None - try: - await self._async_refresh_stream() - finally: - self._schedule_stream_refresh() async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" @@ -128,12 +160,6 @@ class NestCameraBaseEntity(Camera, ABC): self._device.add_update_listener(self.async_write_ha_state) ) - async def async_will_remove_from_hass(self) -> None: - """Invalidates the RTSP token when unloaded.""" - await super().async_will_remove_from_hass() - if self._stream_refresh_unsub: - self._stream_refresh_unsub() - class NestRTSPEntity(NestCameraBaseEntity): """Nest cameras that use RTSP.""" @@ -146,6 +172,7 @@ class NestRTSPEntity(NestCameraBaseEntity): super().__init__(device) self._create_stream_url_lock = asyncio.Lock() self._rtsp_live_stream_trait = device.traits[CameraLiveStreamTrait.NAME] + self._refresh_unsub: Callable[[], None] | None = None @property def use_stream_for_stills(self) -> bool: @@ -173,20 +200,21 @@ class NestRTSPEntity(NestCameraBaseEntity): ) except ApiException as err: raise HomeAssistantError(f"Nest API error: {err}") from err - self._schedule_stream_refresh() + refresh = StreamRefresh( + self.hass, + self._rtsp_stream.expires_at, + self._async_refresh_stream, + ) + self._refresh_unsub = refresh.unsub assert self._rtsp_stream if self._rtsp_stream.expires_at < utcnow(): _LOGGER.warning("Stream already expired") return self._rtsp_stream.rtsp_stream_url - def _stream_expires_at(self) -> datetime.datetime | None: - """Next time when a stream expires.""" - return self._rtsp_stream.expires_at if self._rtsp_stream else None - - async def _async_refresh_stream(self) -> None: + async def _async_refresh_stream(self) -> datetime.datetime | None: """Refresh stream to extend expiration time.""" if not self._rtsp_stream: - return + return None _LOGGER.debug("Extending RTSP stream") try: self._rtsp_stream = await self._rtsp_stream.extend_rtsp_stream() @@ -197,14 +225,17 @@ class NestRTSPEntity(NestCameraBaseEntity): if self.stream: await self.stream.stop() self.stream = None - return + return None # Update the stream worker with the latest valid url if self.stream: self.stream.update_source(self._rtsp_stream.rtsp_stream_url) + return self._rtsp_stream.expires_at async def async_will_remove_from_hass(self) -> None: """Invalidates the RTSP token when unloaded.""" await super().async_will_remove_from_hass() + if self._refresh_unsub is not None: + self._refresh_unsub() if self._rtsp_stream: try: await self._rtsp_stream.stop_stream() @@ -220,37 +251,23 @@ class NestWebRTCEntity(NestCameraBaseEntity): """Initialize the camera.""" super().__init__(device) self._webrtc_sessions: dict[str, WebRtcStream] = {} + self._refresh_unsub: dict[str, Callable[[], None]] = {} @property def frontend_stream_type(self) -> StreamType | None: """Return the type of stream supported by this camera.""" return StreamType.WEB_RTC - def _stream_expires_at(self) -> datetime.datetime | None: - """Next time when a stream expires.""" - if not self._webrtc_sessions: - return None - return min(stream.expires_at for stream in self._webrtc_sessions.values()) - - async def _async_refresh_stream(self) -> None: + async def _async_refresh_stream(self, session_id: str) -> datetime.datetime | None: """Refresh stream to extend expiration time.""" - now = utcnow() - for session_id, webrtc_stream in list(self._webrtc_sessions.items()): - if session_id not in self._webrtc_sessions: - continue - if now < (webrtc_stream.expires_at - STREAM_EXPIRATION_BUFFER): - _LOGGER.debug( - "Stream does not yet expire: %s", webrtc_stream.expires_at - ) - continue - _LOGGER.debug("Extending WebRTC stream %s", webrtc_stream.media_session_id) - try: - webrtc_stream = await webrtc_stream.extend_stream() - except ApiException as err: - _LOGGER.debug("Failed to extend stream: %s", err) - else: - if session_id in self._webrtc_sessions: - self._webrtc_sessions[session_id] = webrtc_stream + if not (webrtc_stream := self._webrtc_sessions.get(session_id)): + return None + _LOGGER.debug("Extending WebRTC stream %s", webrtc_stream.media_session_id) + webrtc_stream = await webrtc_stream.extend_stream() + if session_id in self._webrtc_sessions: + self._webrtc_sessions[session_id] = webrtc_stream + return webrtc_stream.expires_at + return None async def async_camera_image( self, width: int | None = None, height: int | None = None @@ -278,7 +295,12 @@ class NestWebRTCEntity(NestCameraBaseEntity): ) self._webrtc_sessions[session_id] = stream send_message(WebRTCAnswer(stream.answer_sdp)) - self._schedule_stream_refresh() + refresh = StreamRefresh( + self.hass, + stream.expires_at, + functools.partial(self._async_refresh_stream, session_id), + ) + self._refresh_unsub[session_id] = refresh.unsub @callback def close_webrtc_session(self, session_id: str) -> None: @@ -287,6 +309,8 @@ class NestWebRTCEntity(NestCameraBaseEntity): _LOGGER.debug( "Closing WebRTC session %s, %s", session_id, stream.media_session_id ) + unsub = self._refresh_unsub.pop(session_id) + unsub() async def stop_stream() -> None: try: diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 500dbc0f46f0..029879f1413c 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -483,6 +483,50 @@ async def test_stream_response_already_expired( assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" +async def test_extending_stream_already_expired( + hass: HomeAssistant, + auth: FakeAuth, + setup_platform: PlatformSetup, + camera_device: None, +) -> None: + """Test a API response when extending the stream returns an expired stream url.""" + now = utcnow() + stream_1_expiration = now + datetime.timedelta(seconds=180) + stream_2_expiration = now + datetime.timedelta(seconds=30) # Will be in the past + stream_3_expiration = now + datetime.timedelta(seconds=600) + auth.responses = [ + make_stream_url_response(stream_1_expiration, token_num=1), + make_stream_url_response(stream_2_expiration, token_num=2), + make_stream_url_response(stream_3_expiration, token_num=3), + ] + await setup_platform() + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == CameraState.STREAMING + + # The stream is expired, but we return it anyway + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.1.streamingToken" + + # Jump to when the stream will be refreshed + await fire_alarm(hass, now + datetime.timedelta(seconds=160)) + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + # The stream will have expired in the past, but 1 minute min refresh interval is applied. + # The stream token is not updated. + await fire_alarm(hass, now + datetime.timedelta(seconds=170)) + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + # Now go past the min update interval and the stream is refreshed + await fire_alarm(hass, now + datetime.timedelta(seconds=225)) + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.3.streamingToken" + + async def test_camera_removed( hass: HomeAssistant, auth: FakeAuth, From f821ddeab8517c75a0579c6ad3306ce63694834e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 10 Nov 2024 15:46:50 +0100 Subject: [PATCH 040/123] Add more f-series models to myuplink (#130283) --- homeassistant/components/myuplink/binary_sensor.py | 6 ++++-- homeassistant/components/myuplink/const.py | 2 ++ homeassistant/components/myuplink/helpers.py | 14 ++++++++++++-- homeassistant/components/myuplink/number.py | 6 ++++-- homeassistant/components/myuplink/sensor.py | 6 ++++-- homeassistant/components/myuplink/switch.py | 6 ++++-- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py index 0ba6ac7b0782..953859986d0a 100644 --- a/homeassistant/components/myuplink/binary_sensor.py +++ b/homeassistant/components/myuplink/binary_sensor.py @@ -12,11 +12,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkConfigEntry, MyUplinkDataCoordinator +from .const import F_SERIES from .entity import MyUplinkEntity, MyUplinkSystemEntity -from .helpers import find_matching_platform +from .helpers import find_matching_platform, transform_model_series CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, BinarySensorEntityDescription]] = { - "F730": { + F_SERIES: { "43161": BinarySensorEntityDescription( key="elect_add", translation_key="elect_add", @@ -50,6 +51,7 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") + prefix = transform_model_series(prefix) return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) diff --git a/homeassistant/components/myuplink/const.py b/homeassistant/components/myuplink/const.py index 3541a8078c3c..6fd354a21ec4 100644 --- a/homeassistant/components/myuplink/const.py +++ b/homeassistant/components/myuplink/const.py @@ -6,3 +6,5 @@ API_ENDPOINT = "https://api.myuplink.com" OAUTH2_AUTHORIZE = "https://api.myuplink.com/oauth/authorize" OAUTH2_TOKEN = "https://api.myuplink.com/oauth/token" OAUTH2_SCOPES = ["WRITESYSTEM", "READSYSTEM", "offline_access"] + +F_SERIES = "f-series" diff --git a/homeassistant/components/myuplink/helpers.py b/homeassistant/components/myuplink/helpers.py index eb4881c410e1..de5486d8dea2 100644 --- a/homeassistant/components/myuplink/helpers.py +++ b/homeassistant/components/myuplink/helpers.py @@ -6,6 +6,8 @@ from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import Platform +from .const import F_SERIES + def find_matching_platform( device_point: DevicePoint, @@ -86,8 +88,9 @@ PARAMETER_ID_TO_EXCLUDE_F730 = ( "47941", "47975", "48009", - "48042", "48072", + "48442", + "49909", "50113", ) @@ -110,7 +113,7 @@ def skip_entity(model: str, device_point: DevicePoint) -> bool: ): return False return True - if "F730" in model: + if model.lower().startswith("f"): # Entity names containing weekdays are used for advanced scheduling in the # heat pump and should not be exposed in the integration if any(d in device_point.parameter_name.lower() for d in WEEKDAYS): @@ -118,3 +121,10 @@ def skip_entity(model: str, device_point: DevicePoint) -> bool: if device_point.parameter_id in PARAMETER_ID_TO_EXCLUDE_F730: return True return False + + +def transform_model_series(prefix: str) -> str: + """Remap all F-series models.""" + if prefix.lower().startswith("f"): + return F_SERIES + return prefix diff --git a/homeassistant/components/myuplink/number.py b/homeassistant/components/myuplink/number.py index 0c7da0c716f2..b05ab5d46c96 100644 --- a/homeassistant/components/myuplink/number.py +++ b/homeassistant/components/myuplink/number.py @@ -10,8 +10,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkConfigEntry, MyUplinkDataCoordinator +from .const import F_SERIES from .entity import MyUplinkEntity -from .helpers import find_matching_platform, skip_entity +from .helpers import find_matching_platform, skip_entity, transform_model_series DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = { "DM": NumberEntityDescription( @@ -22,7 +23,7 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = { } CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, NumberEntityDescription]] = { - "F730": { + F_SERIES: { "40940": NumberEntityDescription( key="degree_minutes", translation_key="degree_minutes", @@ -48,6 +49,7 @@ def get_description(device_point: DevicePoint) -> NumberEntityDescription | None 3. Default to None """ prefix, _, _ = device_point.category.partition(" ") + prefix = transform_model_series(prefix) description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( device_point.parameter_id ) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 7feb20bc093d..ef827fc1fb10 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -25,8 +25,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import MyUplinkConfigEntry, MyUplinkDataCoordinator +from .const import F_SERIES from .entity import MyUplinkEntity -from .helpers import find_matching_platform, skip_entity +from .helpers import find_matching_platform, skip_entity, transform_model_series DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "°C": SensorEntityDescription( @@ -139,7 +140,7 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { MARKER_FOR_UNKNOWN_VALUE = -32768 CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SensorEntityDescription]] = { - "F730": { + F_SERIES: { "43108": SensorEntityDescription( key="fan_mode", translation_key="fan_mode", @@ -200,6 +201,7 @@ def get_description(device_point: DevicePoint) -> SensorEntityDescription | None """ description = None prefix, _, _ = device_point.category.partition(" ") + prefix = transform_model_series(prefix) description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( device_point.parameter_id ) diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py index 5c47c8294fec..75ba6bd7819f 100644 --- a/homeassistant/components/myuplink/switch.py +++ b/homeassistant/components/myuplink/switch.py @@ -12,11 +12,12 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkConfigEntry, MyUplinkDataCoordinator +from .const import F_SERIES from .entity import MyUplinkEntity -from .helpers import find_matching_platform, skip_entity +from .helpers import find_matching_platform, skip_entity, transform_model_series CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SwitchEntityDescription]] = { - "F730": { + F_SERIES: { "50004": SwitchEntityDescription( key="temporary_lux", translation_key="temporary_lux", @@ -47,6 +48,7 @@ def get_description(device_point: DevicePoint) -> SwitchEntityDescription | None 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") + prefix = transform_model_series(prefix) return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) From f6cd74e2d768e83335fd02623d140076ee4b1f88 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Fri, 15 Nov 2024 04:30:37 -0500 Subject: [PATCH 041/123] Make Hydrawise poll non-critical data less frequently (#130289) --- .../components/hydrawise/__init__.py | 23 ++++- .../components/hydrawise/binary_sensor.py | 14 ++- homeassistant/components/hydrawise/const.py | 3 +- .../components/hydrawise/coordinator.py | 96 ++++++++++++++----- homeassistant/components/hydrawise/sensor.py | 49 ++++++---- homeassistant/components/hydrawise/switch.py | 10 +- homeassistant/components/hydrawise/valve.py | 10 +- .../hydrawise/test_binary_sensor.py | 7 +- .../hydrawise/test_entity_availability.py | 5 +- tests/components/hydrawise/test_sensor.py | 38 +++++++- 10 files changed, 177 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index d2af8f37e362..9e402cd49326 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -7,8 +7,12 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import DOMAIN, SCAN_INTERVAL -from .coordinator import HydrawiseDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import ( + HydrawiseMainDataUpdateCoordinator, + HydrawiseUpdateCoordinators, + HydrawiseWaterUseDataUpdateCoordinator, +) PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, @@ -29,9 +33,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]) ) - coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL) - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + main_coordinator = HydrawiseMainDataUpdateCoordinator(hass, hydrawise) + await main_coordinator.async_config_entry_first_refresh() + water_use_coordinator = HydrawiseWaterUseDataUpdateCoordinator( + hass, hydrawise, main_coordinator + ) + await water_use_coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ( + HydrawiseUpdateCoordinators( + main=main_coordinator, + water_use=water_use_coordinator, + ) + ) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 9b6dcadf95f9..34c31d3ad16d 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType from .const import DOMAIN, SERVICE_RESUME, SERVICE_START_WATERING, SERVICE_SUSPEND -from .coordinator import HydrawiseDataUpdateCoordinator +from .coordinator import HydrawiseUpdateCoordinators from .entity import HydrawiseEntity @@ -81,18 +81,16 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Hydrawise binary_sensor platform.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] entities: list[HydrawiseBinarySensor] = [] - for controller in coordinator.data.controllers.values(): + for controller in coordinators.main.data.controllers.values(): entities.extend( - HydrawiseBinarySensor(coordinator, description, controller) + HydrawiseBinarySensor(coordinators.main, description, controller) for description in CONTROLLER_BINARY_SENSORS ) entities.extend( HydrawiseBinarySensor( - coordinator, + coordinators.main, description, controller, sensor_id=sensor.id, @@ -103,7 +101,7 @@ async def async_setup_entry( ) entities.extend( HydrawiseZoneBinarySensor( - coordinator, description, controller, zone_id=zone.id + coordinators.main, description, controller, zone_id=zone.id ) for zone in controller.zones for description in ZONE_BINARY_SENSORS diff --git a/homeassistant/components/hydrawise/const.py b/homeassistant/components/hydrawise/const.py index 47b9bef845e4..633c00ce6590 100644 --- a/homeassistant/components/hydrawise/const.py +++ b/homeassistant/components/hydrawise/const.py @@ -10,7 +10,8 @@ DEFAULT_WATERING_TIME = timedelta(minutes=15) MANUFACTURER = "Hydrawise" -SCAN_INTERVAL = timedelta(seconds=60) +MAIN_SCAN_INTERVAL = timedelta(seconds=60) +WATER_USE_SCAN_INTERVAL = timedelta(minutes=60) SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index 6cd233eb1dfe..e82a4ec15884 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -2,8 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass -from datetime import timedelta +from dataclasses import dataclass, field from pydrawise import Hydrawise from pydrawise.schema import Controller, ControllerWaterUseSummary, Sensor, User, Zone @@ -12,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import now -from .const import DOMAIN, LOGGER +from .const import DOMAIN, LOGGER, MAIN_SCAN_INTERVAL, WATER_USE_SCAN_INTERVAL @dataclass @@ -20,22 +19,39 @@ class HydrawiseData: """Container for data fetched from the Hydrawise API.""" user: User - controllers: dict[int, Controller] - zones: dict[int, Zone] - sensors: dict[int, Sensor] - daily_water_summary: dict[int, ControllerWaterUseSummary] + controllers: dict[int, Controller] = field(default_factory=dict) + zones: dict[int, Zone] = field(default_factory=dict) + sensors: dict[int, Sensor] = field(default_factory=dict) + daily_water_summary: dict[int, ControllerWaterUseSummary] = field( + default_factory=dict + ) + + +@dataclass +class HydrawiseUpdateCoordinators: + """Container for all Hydrawise DataUpdateCoordinator instances.""" + + main: HydrawiseMainDataUpdateCoordinator + water_use: HydrawiseWaterUseDataUpdateCoordinator class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): - """The Hydrawise Data Update Coordinator.""" + """Base class for Hydrawise Data Update Coordinators.""" api: Hydrawise - def __init__( - self, hass: HomeAssistant, api: Hydrawise, scan_interval: timedelta - ) -> None: + +class HydrawiseMainDataUpdateCoordinator(HydrawiseDataUpdateCoordinator): + """The main Hydrawise Data Update Coordinator. + + This fetches the primary state data for Hydrawise controllers and zones + at a relatively frequent interval so that the primary functions of the + integration are updated in a timely manner. + """ + + def __init__(self, hass: HomeAssistant, api: Hydrawise) -> None: """Initialize HydrawiseDataUpdateCoordinator.""" - super().__init__(hass, LOGGER, name=DOMAIN, update_interval=scan_interval) + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=MAIN_SCAN_INTERVAL) self.api = api async def _async_update_data(self) -> HydrawiseData: @@ -43,28 +59,56 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): # Don't fetch zones. We'll fetch them for each controller later. # This is to prevent 502 errors in some cases. # See: https://github.com/home-assistant/core/issues/120128 - user = await self.api.get_user(fetch_zones=False) - controllers = {} - zones = {} - sensors = {} - daily_water_summary: dict[int, ControllerWaterUseSummary] = {} - for controller in user.controllers: - controllers[controller.id] = controller + data = HydrawiseData(user=await self.api.get_user(fetch_zones=False)) + for controller in data.user.controllers: + data.controllers[controller.id] = controller controller.zones = await self.api.get_zones(controller) for zone in controller.zones: - zones[zone.id] = zone + data.zones[zone.id] = zone for sensor in controller.sensors: - sensors[sensor.id] = sensor + data.sensors[sensor.id] = sensor + return data + + +class HydrawiseWaterUseDataUpdateCoordinator(HydrawiseDataUpdateCoordinator): + """Data Update Coordinator for Hydrawise Water Use. + + This fetches data that is more expensive for the Hydrawise API to compute + at a less frequent interval as to not overload the Hydrawise servers. + """ + + _main_coordinator: HydrawiseMainDataUpdateCoordinator + + def __init__( + self, + hass: HomeAssistant, + api: Hydrawise, + main_coordinator: HydrawiseMainDataUpdateCoordinator, + ) -> None: + """Initialize HydrawiseWaterUseDataUpdateCoordinator.""" + super().__init__( + hass, + LOGGER, + name=f"{DOMAIN} water use", + update_interval=WATER_USE_SCAN_INTERVAL, + ) + self.api = api + self._main_coordinator = main_coordinator + + async def _async_update_data(self) -> HydrawiseData: + """Fetch the latest data from Hydrawise.""" + daily_water_summary: dict[int, ControllerWaterUseSummary] = {} + for controller in self._main_coordinator.data.controllers.values(): daily_water_summary[controller.id] = await self.api.get_water_use_summary( controller, now().replace(hour=0, minute=0, second=0, microsecond=0), now(), ) - + main_data = self._main_coordinator.data return HydrawiseData( - user=user, - controllers=controllers, - zones=zones, - sensors=sensors, + user=main_data.user, + controllers=main_data.controllers, + zones=main_data.zones, + sensors=main_data.sensors, daily_water_summary=daily_water_summary, ) diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 563af893700a..1d8c75d5437a 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import DOMAIN -from .coordinator import HydrawiseDataUpdateCoordinator +from .coordinator import HydrawiseUpdateCoordinators from .entity import HydrawiseEntity @@ -92,7 +92,7 @@ def _get_controller_daily_total_water_use(sensor: HydrawiseSensor) -> float | No return daily_water_summary.total_use -CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( +WATER_USE_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( HydrawiseSensorEntityDescription( key="daily_active_water_time", translation_key="daily_active_water_time", @@ -103,6 +103,16 @@ CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( ) +WATER_USE_ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( + HydrawiseSensorEntityDescription( + key="daily_active_water_time", + translation_key="daily_active_water_time", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=_get_zone_daily_active_water_time, + ), +) + FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( HydrawiseSensorEntityDescription( key="daily_total_water_use", @@ -150,13 +160,6 @@ ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.MINUTES, value_fn=_get_zone_watering_time, ), - HydrawiseSensorEntityDescription( - key="daily_active_water_time", - translation_key="daily_active_water_time", - device_class=SensorDeviceClass.DURATION, - native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=_get_zone_daily_active_water_time, - ), ) FLOW_MEASUREMENT_KEYS = [x.key for x in FLOW_CONTROLLER_SENSORS] @@ -168,29 +171,37 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Hydrawise sensor platform.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] entities: list[HydrawiseSensor] = [] - for controller in coordinator.data.controllers.values(): + for controller in coordinators.main.data.controllers.values(): entities.extend( - HydrawiseSensor(coordinator, description, controller) - for description in CONTROLLER_SENSORS + HydrawiseSensor(coordinators.water_use, description, controller) + for description in WATER_USE_CONTROLLER_SENSORS ) entities.extend( - HydrawiseSensor(coordinator, description, controller, zone_id=zone.id) + HydrawiseSensor( + coordinators.water_use, description, controller, zone_id=zone.id + ) + for zone in controller.zones + for description in WATER_USE_ZONE_SENSORS + ) + entities.extend( + HydrawiseSensor(coordinators.main, description, controller, zone_id=zone.id) for zone in controller.zones for description in ZONE_SENSORS ) - if coordinator.data.daily_water_summary[controller.id].total_use is not None: + if ( + coordinators.water_use.data.daily_water_summary[controller.id].total_use + is not None + ): # we have a flow sensor for this controller entities.extend( - HydrawiseSensor(coordinator, description, controller) + HydrawiseSensor(coordinators.water_use, description, controller) for description in FLOW_CONTROLLER_SENSORS ) entities.extend( HydrawiseSensor( - coordinator, + coordinators.water_use, description, controller, zone_id=zone.id, diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 001a8e399ee8..1addaf1ec927 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import DEFAULT_WATERING_TIME, DOMAIN -from .coordinator import HydrawiseDataUpdateCoordinator +from .coordinator import HydrawiseUpdateCoordinators from .entity import HydrawiseEntity @@ -66,12 +66,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Hydrawise switch platform.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - HydrawiseSwitch(coordinator, description, controller, zone_id=zone.id) - for controller in coordinator.data.controllers.values() + HydrawiseSwitch(coordinators.main, description, controller, zone_id=zone.id) + for controller in coordinators.main.data.controllers.values() for zone in controller.zones for description in SWITCH_TYPES ) diff --git a/homeassistant/components/hydrawise/valve.py b/homeassistant/components/hydrawise/valve.py index 6ceb3673c718..37f196bc0543 100644 --- a/homeassistant/components/hydrawise/valve.py +++ b/homeassistant/components/hydrawise/valve.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import HydrawiseDataUpdateCoordinator +from .coordinator import HydrawiseUpdateCoordinators from .entity import HydrawiseEntity VALVE_TYPES: tuple[ValveEntityDescription, ...] = ( @@ -34,12 +34,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Hydrawise valve platform.""" - coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - HydrawiseValve(coordinator, description, controller, zone_id=zone.id) - for controller in coordinator.data.controllers.values() + HydrawiseValve(coordinators.main, description, controller, zone_id=zone.id) + for controller in coordinators.main.data.controllers.values() for zone in controller.zones for description in VALVE_TYPES ) diff --git a/tests/components/hydrawise/test_binary_sensor.py b/tests/components/hydrawise/test_binary_sensor.py index a42f9b1c0442..40cd32920b02 100644 --- a/tests/components/hydrawise/test_binary_sensor.py +++ b/tests/components/hydrawise/test_binary_sensor.py @@ -9,7 +9,7 @@ from freezegun.api import FrozenDateTimeFactory from pydrawise.schema import Controller from syrupy.assertion import SnapshotAssertion -from homeassistant.components.hydrawise.const import SCAN_INTERVAL +from homeassistant.components.hydrawise.const import MAIN_SCAN_INTERVAL from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -42,7 +42,8 @@ async def test_update_data_fails( # Make the coordinator refresh data. mock_pydrawise.get_user.reset_mock(return_value=True) mock_pydrawise.get_user.side_effect = ClientError - freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) + mock_pydrawise.get_water_use_summary.side_effect = ClientError + freezer.tick(MAIN_SCAN_INTERVAL + timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -61,7 +62,7 @@ async def test_controller_offline( """Test the binary_sensor for the controller being online.""" # Make the coordinator refresh data. controller.online = False - freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) + freezer.tick(MAIN_SCAN_INTERVAL + timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/hydrawise/test_entity_availability.py b/tests/components/hydrawise/test_entity_availability.py index 58ded5fe6c3e..27587425c31d 100644 --- a/tests/components/hydrawise/test_entity_availability.py +++ b/tests/components/hydrawise/test_entity_availability.py @@ -8,7 +8,7 @@ from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory from pydrawise.schema import Controller -from homeassistant.components.hydrawise.const import SCAN_INTERVAL +from homeassistant.components.hydrawise.const import WATER_USE_SCAN_INTERVAL from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -42,7 +42,8 @@ async def test_api_offline( config_entry = await mock_add_config_entry() mock_pydrawise.get_user.reset_mock(return_value=True) mock_pydrawise.get_user.side_effect = ClientError - freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) + mock_pydrawise.get_water_use_summary.side_effect = ClientError + freezer.tick(WATER_USE_SCAN_INTERVAL + timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() _test_availability(hass, config_entry, entity_registry) diff --git a/tests/components/hydrawise/test_sensor.py b/tests/components/hydrawise/test_sensor.py index b9ff99f0013c..1c14a07f1823 100644 --- a/tests/components/hydrawise/test_sensor.py +++ b/tests/components/hydrawise/test_sensor.py @@ -1,12 +1,18 @@ """Test Hydrawise sensor.""" from collections.abc import Awaitable, Callable -from unittest.mock import patch +from datetime import timedelta +from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory from pydrawise.schema import Controller, ControllerWaterUseSummary, User, Zone import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.hydrawise.const import ( + MAIN_SCAN_INTERVAL, + WATER_USE_SCAN_INTERVAL, +) from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -16,7 +22,7 @@ from homeassistant.util.unit_system import ( UnitSystem, ) -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.freeze_time("2023-10-01 00:00:00+00:00") @@ -50,6 +56,34 @@ async def test_suspended_state( assert next_cycle.state == "unknown" +@pytest.mark.freeze_time("2024-11-01 00:00:00+00:00") +async def test_usage_refresh( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + controller_water_use_summary: ControllerWaterUseSummary, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that water usage summaries refresh less frequently than other data.""" + assert hass.states.get("sensor.zone_one_daily_active_water_use") is not None + mock_pydrawise.get_water_use_summary.assert_called_once() + + # Make the coordinator refresh data. + mock_pydrawise.get_water_use_summary.reset_mock() + freezer.tick(MAIN_SCAN_INTERVAL + timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + # Make sure we didn't fetch water use summary again. + mock_pydrawise.get_water_use_summary.assert_not_called() + + # Wait for enough time to pass for a water use summary fetch. + mock_pydrawise.get_water_use_summary.return_value = controller_water_use_summary + freezer.tick(WATER_USE_SCAN_INTERVAL + timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_pydrawise.get_water_use_summary.assert_called_once() + + async def test_no_sensor_and_water_state( hass: HomeAssistant, controller: Controller, From 282f92e5f34f2c09ee70f74f8c9e70561abbf23d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Nov 2024 10:27:40 -0800 Subject: [PATCH 042/123] Ignore WebRTC candidates for nest cameras (#130294) --- homeassistant/components/nest/camera.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 4cb88e636416..0a46d67a3ad5 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -19,6 +19,7 @@ from google_nest_sdm.camera_traits import ( from google_nest_sdm.device import Device from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.exceptions import ApiException +from webrtc_models import RTCIceCandidate from homeassistant.components.camera import ( Camera, @@ -302,6 +303,12 @@ class NestWebRTCEntity(NestCameraBaseEntity): ) self._refresh_unsub[session_id] = refresh.unsub + async def async_on_webrtc_candidate( + self, session_id: str, candidate: RTCIceCandidate + ) -> None: + """Ignore WebRTC candidates for Nest cloud based cameras.""" + return + @callback def close_webrtc_session(self, session_id: str) -> None: """Close a WebRTC session.""" From afec354b8460e223681186f0b9ac130e59f867f2 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 10 Nov 2024 20:11:42 +0100 Subject: [PATCH 043/123] Avoid Shelly data update during shutdown (#130301) --- homeassistant/components/shelly/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 6332e139244b..a66fbb20f481 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -603,7 +603,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): async def _async_update_data(self) -> None: """Fetch data.""" - if self.update_sleep_period(): + if self.update_sleep_period() or self.hass.is_stopping: return if self.sleep_period: From 218eedfd934d2e95f9ff04513c2025a7edd57036 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 10 Nov 2024 23:40:23 +0100 Subject: [PATCH 044/123] Fix Homekit error handling alarm state unknown or unavailable (#130311) --- .../homekit/type_security_systems.py | 12 +++--- .../homekit/test_type_security_systems.py | 37 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 9f3f183f11fd..8634589cb5f5 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -18,6 +18,8 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import State, callback @@ -152,12 +154,12 @@ class SecuritySystem(HomeAccessory): @callback def async_update_state(self, new_state: State) -> None: """Update security state after state changed.""" - hass_state = None - if new_state and new_state.state == "None": - # Bail out early for no state + hass_state: str | AlarmControlPanelState = new_state.state + if hass_state in {"None", STATE_UNKNOWN, STATE_UNAVAILABLE}: + # Bail out early for no state, unknown or unavailable return - if new_state and new_state.state is not None: - hass_state = AlarmControlPanelState(new_state.state) + if hass_state is not None: + hass_state = AlarmControlPanelState(hass_state) if ( hass_state and (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 8377d847a7ac..94b0e68e76d6 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -10,7 +10,12 @@ from homeassistant.components.alarm_control_panel import ( ) from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_security_systems import SecuritySystem -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service @@ -307,3 +312,33 @@ async def test_supported_states(hass: HomeAssistant, hk_driver) -> None: for val in valid_target_values.values(): assert val in test_config.get("target_values") + + +@pytest.mark.parametrize( + ("state"), + [ + (None), + ("None"), + (STATE_UNKNOWN), + (STATE_UNAVAILABLE), + ], +) +async def test_handle_non_alarm_states( + hass: HomeAssistant, hk_driver, events: list[Event], state: str +) -> None: + """Test we can handle states that should not raise.""" + code = "1234" + config = {ATTR_CODE: code} + entity_id = "alarm_control_panel.test" + + hass.states.async_set(entity_id, state) + await hass.async_block_till_done() + acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) + acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 11 # AlarmSystem + + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 3 From 465d8b2ee28846cd07993d4394870a2d8bc809dc Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Tue, 12 Nov 2024 16:26:28 +0900 Subject: [PATCH 045/123] Fix fan's warning TURN_ON, TURN_OFF (#130327) Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/fan.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py index 187cc74b3eb3..edcadf2598ab 100644 --- a/homeassistant/components/lg_thinq/fan.py +++ b/homeassistant/components/lg_thinq/fan.py @@ -72,8 +72,11 @@ class ThinQFanEntity(ThinQEntity, FanEntity): super().__init__(coordinator, entity_description, property_id) self._ordered_named_fan_speeds = [] - self._attr_supported_features |= FanEntityFeature.SET_SPEED - + self._attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) if (fan_modes := self.data.fan_modes) is not None: self._attr_speed_count = len(fan_modes) if self.speed_count == 4: @@ -98,7 +101,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): self._attr_percentage = 0 _LOGGER.debug( - "[%s:%s] update status: %s -> %s (percntage=%s)", + "[%s:%s] update status: %s -> %s (percentage=%s)", self.coordinator.device_name, self.property_id, self.data.is_on, @@ -120,7 +123,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): return _LOGGER.debug( - "[%s:%s] async_set_percentage. percntage=%s, value=%s", + "[%s:%s] async_set_percentage. percentage=%s, value=%s", self.coordinator.device_name, self.property_id, percentage, From ce92f3de44ff433de7b56e57c856dd8bc8ebcb04 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:30:27 +0100 Subject: [PATCH 046/123] Bump python-linkplay to 0.0.20 (#130348) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 9ddb6abf0939..e74d22b8207e 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.0.18"], + "requirements": ["python-linkplay==0.0.20"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a7992c184308..2b7e5a401217 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2356,7 +2356,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.7 # homeassistant.components.linkplay -python-linkplay==0.0.18 +python-linkplay==0.0.20 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0cd3b94af4f3..3376e6e55e8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1883,7 +1883,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.7 # homeassistant.components.linkplay -python-linkplay==0.0.18 +python-linkplay==0.0.20 # homeassistant.components.matter python-matter-server==6.6.0 From a86ff41bbc9272d0ed9f1a6964a1b4b5062e50a3 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:49:56 +0100 Subject: [PATCH 047/123] Add seek support to LinkPlay (#130349) --- homeassistant/components/linkplay/media_player.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index a625412852ee..832b56d0891d 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -296,6 +296,11 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): except ValueError as err: raise HomeAssistantError(err) from err + @exception_wrap + async def async_media_seek(self, position: float) -> None: + """Seek to a position.""" + await self._bridge.player.seek(round(position)) + @exception_wrap async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" @@ -381,9 +386,9 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): ) self._attr_source = SOURCE_MAP.get(self._bridge.player.play_mode, "other") - self._attr_media_position = self._bridge.player.current_position / 1000 + self._attr_media_position = self._bridge.player.current_position_in_seconds self._attr_media_position_updated_at = utcnow() - self._attr_media_duration = self._bridge.player.total_length / 1000 + self._attr_media_duration = self._bridge.player.total_length_in_seconds self._attr_media_artist = self._bridge.player.artist self._attr_media_title = self._bridge.player.title self._attr_media_album_name = self._bridge.player.album From 8d05183de275e431b1497908c630f3c9b60266a8 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:46:02 +0100 Subject: [PATCH 048/123] Add Spotify and Tidal to playingmode mapping (#130351) --- homeassistant/components/linkplay/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 832b56d0891d..c29c29785228 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -69,6 +69,8 @@ SOURCE_MAP: dict[PlayingMode, str] = { PlayingMode.FM: "FM Radio", PlayingMode.RCA: "RCA", PlayingMode.UDISK: "USB", + PlayingMode.SPOTIFY: "Spotify", + PlayingMode.TIDAL: "Tidal", PlayingMode.FOLLOWER: "Follower", } From 9292b6da3dd22b494848435174d93a9167dfbaee Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 14 Nov 2024 04:11:44 -0500 Subject: [PATCH 049/123] Disable brightness from devices with no display in Cambridge Audio (#130369) --- homeassistant/components/cambridge_audio/manifest.json | 2 +- homeassistant/components/cambridge_audio/select.py | 7 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cambridge_audio/manifest.json b/homeassistant/components/cambridge_audio/manifest.json index edacd17f54dd..c359ca14a21a 100644 --- a/homeassistant/components/cambridge_audio/manifest.json +++ b/homeassistant/components/cambridge_audio/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aiostreammagic"], - "requirements": ["aiostreammagic==2.8.4"], + "requirements": ["aiostreammagic==2.8.5"], "zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."] } diff --git a/homeassistant/components/cambridge_audio/select.py b/homeassistant/components/cambridge_audio/select.py index ca6eebdec6b8..c99abc853e55 100644 --- a/homeassistant/components/cambridge_audio/select.py +++ b/homeassistant/components/cambridge_audio/select.py @@ -51,8 +51,13 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = ( CambridgeAudioSelectEntityDescription( key="display_brightness", translation_key="display_brightness", - options=[x.value for x in DisplayBrightness], + options=[ + DisplayBrightness.BRIGHT.value, + DisplayBrightness.DIM.value, + DisplayBrightness.OFF.value, + ], entity_category=EntityCategory.CONFIG, + load_fn=lambda client: client.display.brightness != DisplayBrightness.NONE, value_fn=lambda client: client.display.brightness, set_value_fn=lambda client, value: client.set_display_brightness( DisplayBrightness(value) diff --git a/requirements_all.txt b/requirements_all.txt index 2b7e5a401217..2d1b21855bee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -381,7 +381,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.8.4 +aiostreammagic==2.8.5 # homeassistant.components.switcher_kis aioswitcher==4.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3376e6e55e8a..028eba2119ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -363,7 +363,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.cambridge_audio -aiostreammagic==2.8.4 +aiostreammagic==2.8.5 # homeassistant.components.switcher_kis aioswitcher==4.4.0 From a12c76dbdde9e2360fd4af5645d5bf9552782fc8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Nov 2024 20:16:46 +0100 Subject: [PATCH 050/123] Use f-strings in go2rtc code and test and do not use abbreviation (#130158) --- homeassistant/components/go2rtc/__init__.py | 10 +++++----- tests/components/go2rtc/test_init.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index ca4aeeed9384..e44361f69a4c 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -244,21 +244,21 @@ class WebRTCProvider(CameraWebRTCProvider): if self._data.managed: # HA manages the go2rtc instance - stream_org_name = camera.entity_id + "_orginal" + stream_original_name = f"{camera.entity_id}_orginal" stream_redirect_sources = [ - f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_org_name}", - f"ffmpeg:{stream_org_name}#audio=opus", + f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}", + f"ffmpeg:{stream_original_name}#audio=opus", ] if ( - (stream_org := streams.get(stream_org_name)) is None + (stream_org := streams.get(stream_original_name)) is None or not any( stream_source == producer.url for producer in stream_org.producers ) or (stream_redirect := streams.get(camera.entity_id)) is None or stream_redirect_sources != [p.url for p in stream_redirect.producers] ): - await self._rest_client.streams.add(stream_org_name, stream_source) + await self._rest_client.streams.add(stream_original_name, stream_source) await self._rest_client.streams.add( camera.entity_id, stream_redirect_sources ) diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index ea1971a31d9e..e085bab31b32 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -313,7 +313,7 @@ async def test_setup_managed( camera = init_test_integration entity_id = camera.entity_id - stream_name_orginal = camera.entity_id + "_orginal" + stream_name_orginal = f"{camera.entity_id}_orginal" assert camera.frontend_stream_type == StreamType.HLS assert await async_setup_component(hass, DOMAIN, config) From 83162c1461b6fff6e6ce3a95756588057930d4f8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Nov 2024 20:38:38 +0100 Subject: [PATCH 051/123] Fix typo in go2rtc (#130165) Fix typo in original --- homeassistant/components/go2rtc/__init__.py | 2 +- tests/components/go2rtc/test_init.py | 26 ++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index e44361f69a4c..04b5b9f93173 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -244,7 +244,7 @@ class WebRTCProvider(CameraWebRTCProvider): if self._data.managed: # HA manages the go2rtc instance - stream_original_name = f"{camera.entity_id}_orginal" + stream_original_name = f"{camera.entity_id}_original" stream_redirect_sources = [ f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}", f"ffmpeg:{stream_original_name}#audio=opus", diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index e085bab31b32..ec5867761428 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -313,7 +313,7 @@ async def test_setup_managed( camera = init_test_integration entity_id = camera.entity_id - stream_name_orginal = f"{camera.entity_id}_orginal" + stream_name_original = f"{camera.entity_id}_original" assert camera.frontend_stream_type == StreamType.HLS assert await async_setup_component(hass, DOMAIN, config) @@ -346,12 +346,12 @@ async def test_setup_managed( await test() stream_added_calls = [ - call(stream_name_orginal, "rtsp://stream"), + call(stream_name_original, "rtsp://stream"), call( entity_id, [ - f"rtsp://127.0.0.1:18554/{stream_name_orginal}", - f"ffmpeg:{stream_name_orginal}#audio=opus", + f"rtsp://127.0.0.1:18554/{stream_name_original}", + f"ffmpeg:{stream_name_original}#audio=opus", ], ), ] @@ -362,8 +362,8 @@ async def test_setup_managed( rest_client.streams.list.return_value = { entity_id: Stream( [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"), - Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"), + Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), + Producer(f"ffmpeg:{stream_name_original}#audio=opus"), ] ) } @@ -377,11 +377,11 @@ async def test_setup_managed( # Stream original source different rest_client.streams.add.reset_mock() rest_client.streams.list.return_value = { - stream_name_orginal: Stream([Producer("rtsp://different")]), + stream_name_original: Stream([Producer("rtsp://different")]), entity_id: Stream( [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"), - Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"), + Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), + Producer(f"ffmpeg:{stream_name_original}#audio=opus"), ] ), } @@ -395,7 +395,7 @@ async def test_setup_managed( # Stream source different rest_client.streams.add.reset_mock() rest_client.streams.list.return_value = { - stream_name_orginal: Stream([Producer("rtsp://stream")]), + stream_name_original: Stream([Producer("rtsp://stream")]), entity_id: Stream([Producer("rtsp://different")]), } @@ -408,11 +408,11 @@ async def test_setup_managed( # If the stream is already added, the stream should not be added again. rest_client.streams.add.reset_mock() rest_client.streams.list.return_value = { - stream_name_orginal: Stream([Producer("rtsp://stream")]), + stream_name_original: Stream([Producer("rtsp://stream")]), entity_id: Stream( [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"), - Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"), + Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), + Producer(f"ffmpeg:{stream_name_original}#audio=opus"), ] ), } From 70ef3a355ce0eb550de42ca1135b837964ed6b77 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 12 Nov 2024 11:53:14 +0100 Subject: [PATCH 052/123] Go2rtc bump and set ffmpeg logs to debug (#130371) --- Dockerfile | 2 +- homeassistant/components/go2rtc/__init__.py | 83 ++------ homeassistant/components/go2rtc/const.py | 1 - homeassistant/components/go2rtc/server.py | 8 +- script/hassfest/docker.py | 2 +- tests/components/go2rtc/test_init.py | 223 +++----------------- 6 files changed, 51 insertions(+), 268 deletions(-) diff --git a/Dockerfile b/Dockerfile index b6d571f308e6..a023b346d596 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ RUN \ "armv7") go2rtc_suffix='arm' ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \ esac \ - && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ + && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ && chmod +x /bin/go2rtc \ # Verify go2rtc can be executed && go2rtc --version diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 04b5b9f93173..fc91ef5e546e 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -1,8 +1,5 @@ """The go2rtc component.""" -from __future__ import annotations - -from dataclasses import dataclass import logging import shutil @@ -41,13 +38,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from homeassistant.util.package import is_docker_env -from .const import ( - CONF_DEBUG_UI, - DEBUG_UI_URL_MESSAGE, - DOMAIN, - HA_MANAGED_RTSP_PORT, - HA_MANAGED_URL, -) +from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL from .server import Server _LOGGER = logging.getLogger(__name__) @@ -94,22 +85,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN) +_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN) _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) -@dataclass(frozen=True) -class Go2RtcData: - """Data for go2rtc.""" - - url: str - managed: bool - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up WebRTC.""" url: str | None = None - managed = False if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config: await _remove_go2rtc_entries(hass) return True @@ -144,9 +126,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) url = HA_MANAGED_URL - managed = True - hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed) + hass.data[_DATA_GO2RTC] = url discovery_flow.async_create_flow( hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} ) @@ -161,32 +142,28 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up go2rtc from a config entry.""" - data = hass.data[_DATA_GO2RTC] + url = hass.data[_DATA_GO2RTC] # Validate the server URL try: - client = Go2RtcRestClient(async_get_clientsession(hass), data.url) + client = Go2RtcRestClient(async_get_clientsession(hass), url) await client.validate_server_version() except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( - f"Could not connect to go2rtc instance on {data.url}" + f"Could not connect to go2rtc instance on {url}" ) from err - _LOGGER.warning( - "Could not connect to go2rtc instance on %s (%s)", data.url, err - ) + _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False except Go2RtcVersionError as err: raise ConfigEntryNotReady( f"The go2rtc server version is not supported, {err}" ) from err except Exception as err: # noqa: BLE001 - _LOGGER.warning( - "Could not connect to go2rtc instance on %s (%s)", data.url, err - ) + _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False - provider = WebRTCProvider(hass, data) + provider = WebRTCProvider(hass, url) async_register_webrtc_provider(hass, provider) return True @@ -204,12 +181,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None: class WebRTCProvider(CameraWebRTCProvider): """WebRTC provider.""" - def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None: + def __init__(self, hass: HomeAssistant, url: str) -> None: """Initialize the WebRTC provider.""" self._hass = hass - self._data = data + self._url = url self._session = async_get_clientsession(hass) - self._rest_client = Go2RtcRestClient(self._session, data.url) + self._rest_client = Go2RtcRestClient(self._session, url) self._sessions: dict[str, Go2RtcWsClient] = {} @property @@ -231,7 +208,7 @@ class WebRTCProvider(CameraWebRTCProvider): ) -> None: """Handle the WebRTC offer and return the answer via the provided callback.""" self._sessions[session_id] = ws_client = Go2RtcWsClient( - self._session, self._data.url, source=camera.entity_id + self._session, self._url, source=camera.entity_id ) if not (stream_source := await camera.stream_source()): @@ -242,34 +219,18 @@ class WebRTCProvider(CameraWebRTCProvider): streams = await self._rest_client.streams.list() - if self._data.managed: - # HA manages the go2rtc instance - stream_original_name = f"{camera.entity_id}_original" - stream_redirect_sources = [ - f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}", - f"ffmpeg:{stream_original_name}#audio=opus", - ] - - if ( - (stream_org := streams.get(stream_original_name)) is None - or not any( - stream_source == producer.url for producer in stream_org.producers - ) - or (stream_redirect := streams.get(camera.entity_id)) is None - or stream_redirect_sources != [p.url for p in stream_redirect.producers] - ): - await self._rest_client.streams.add(stream_original_name, stream_source) - await self._rest_client.streams.add( - camera.entity_id, stream_redirect_sources - ) - - # go2rtc instance is managed outside HA - elif (stream_org := streams.get(camera.entity_id)) is None or not any( - stream_source == producer.url for producer in stream_org.producers + if (stream := streams.get(camera.entity_id)) is None or not any( + stream_source == producer.url for producer in stream.producers ): await self._rest_client.streams.add( camera.entity_id, - [stream_source, f"ffmpeg:{camera.entity_id}#audio=opus"], + [ + stream_source, + # We are setting any ffmpeg rtsp related logs to debug + # Connection problems to the camera will be logged by the first stream + # Therefore setting it to debug will not hide any important logs + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + ], ) @callback diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index 3c4dc9a95009..d33ae3e38975 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,4 +6,3 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" -HA_MANAGED_RTSP_PORT = 18554 diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index 91f4433546ca..6699ee4d8a29 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL +from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL _LOGGER = logging.getLogger(__name__) _TERMINATE_TIMEOUT = 5 @@ -33,7 +33,7 @@ api: listen: "{api_ip}:{api_port}" rtsp: - listen: "127.0.0.1:{rtsp_port}" + listen: "127.0.0.1:18554" webrtc: listen: ":18555/tcp" @@ -68,9 +68,7 @@ def _create_temp_file(api_ip: str) -> str: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: file.write( _GO2RTC_CONFIG_FORMAT.format( - api_ip=api_ip, - api_port=HA_MANAGED_API_PORT, - rtsp_port=HA_MANAGED_RTSP_PORT, + api_ip=api_ip, api_port=HA_MANAGED_API_PORT ).encode() ) return file.name diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 083cdaba1a90..9d38d8f71282 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -112,7 +112,7 @@ LABEL "com.github.actions.icon"="terminal" LABEL "com.github.actions.color"="gray-dark" """ -_GO2RTC_VERSION = "1.9.6" +_GO2RTC_VERSION = "1.9.7" def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]: diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index ec5867761428..9388110366ec 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Generator import logging from typing import NamedTuple -from unittest.mock import AsyncMock, Mock, call, patch +from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from go2rtc_client import Stream @@ -238,7 +238,11 @@ async def _test_setup_and_signaling( await test() rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + entity_id, + [ + "rtsp://stream", + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + ], ) # Stream exists but the source is different @@ -252,7 +256,11 @@ async def _test_setup_and_signaling( await test() rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] + entity_id, + [ + "rtsp://stream", + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + ], ) # If the stream is already added, the stream should not be added again. @@ -296,7 +304,7 @@ async def _test_setup_and_signaling( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_managed( +async def test_setup_go_binary( hass: HomeAssistant, rest_client: AsyncMock, ws_client: Mock, @@ -308,131 +316,15 @@ async def test_setup_managed( config: ConfigType, ui_enabled: bool, ) -> None: - """Test the go2rtc setup with managed go2rtc instance.""" + """Test the go2rtc config entry with binary.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry - camera = init_test_integration - entity_id = camera.entity_id - stream_name_original = f"{camera.entity_id}_original" - assert camera.frontend_stream_type == StreamType.HLS + def after_setup() -> None: + server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) + server_start.assert_called_once() - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done(wait_background_tasks=True) - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - assert config_entries[0].state == ConfigEntryState.LOADED - server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled) - server_start.assert_called_once() - - receive_message_callback = Mock(spec_set=WebRTCSendMessage) - - async def test() -> None: - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - ws_client.send.assert_called_once_with( - WebRTCOffer( - OFFER_SDP, - camera.async_get_webrtc_client_configuration().configuration.ice_servers, - ) - ) - ws_client.subscribe.assert_called_once() - - # Simulate the answer from the go2rtc server - callback = ws_client.subscribe.call_args[0][0] - callback(WebRTCAnswer(ANSWER_SDP)) - receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) - - await test() - - stream_added_calls = [ - call(stream_name_original, "rtsp://stream"), - call( - entity_id, - [ - f"rtsp://127.0.0.1:18554/{stream_name_original}", - f"ffmpeg:{stream_name_original}#audio=opus", - ], - ), - ] - assert rest_client.streams.add.call_args_list == stream_added_calls - - # Stream original missing - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - entity_id: Stream( - [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), - Producer(f"ffmpeg:{stream_name_original}#audio=opus"), - ] - ) - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - assert rest_client.streams.add.call_args_list == stream_added_calls - - # Stream original source different - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - stream_name_original: Stream([Producer("rtsp://different")]), - entity_id: Stream( - [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), - Producer(f"ffmpeg:{stream_name_original}#audio=opus"), - ] - ), - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - assert rest_client.streams.add.call_args_list == stream_added_calls - - # Stream source different - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - stream_name_original: Stream([Producer("rtsp://stream")]), - entity_id: Stream([Producer("rtsp://different")]), - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - assert rest_client.streams.add.call_args_list == stream_added_calls - - # If the stream is already added, the stream should not be added again. - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - stream_name_original: Stream([Producer("rtsp://stream")]), - entity_id: Stream( - [ - Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"), - Producer(f"ffmpeg:{stream_name_original}#audio=opus"), - ] - ), - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - rest_client.streams.add.assert_not_called() - assert isinstance(camera._webrtc_provider, WebRTCProvider) - - # Set stream source to None and provider should be skipped - rest_client.streams.list.return_value = {} - receive_message_callback.reset_mock() - camera.set_stream_source(None) - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - receive_message_callback.assert_called_once_with( - WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") + await _test_setup_and_signaling( + hass, rest_client, ws_client, config, after_setup, init_test_integration ) await hass.async_stop() @@ -448,7 +340,7 @@ async def test_setup_managed( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_self_hosted( +async def test_setup_go( hass: HomeAssistant, rest_client: AsyncMock, ws_client: Mock, @@ -458,83 +350,16 @@ async def test_setup_self_hosted( mock_is_docker_env: Mock, has_go2rtc_entry: bool, ) -> None: - """Test the go2rtc with selfhosted go2rtc instance.""" + """Test the go2rtc config entry without binary.""" assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}} - camera = init_test_integration - entity_id = camera.entity_id - assert camera.frontend_stream_type == StreamType.HLS + def after_setup() -> None: + server.assert_not_called() - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done(wait_background_tasks=True) - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - assert config_entries[0].state == ConfigEntryState.LOADED - server.assert_not_called() - - receive_message_callback = Mock(spec_set=WebRTCSendMessage) - - async def test() -> None: - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - ws_client.send.assert_called_once_with( - WebRTCOffer( - OFFER_SDP, - camera.async_get_webrtc_client_configuration().configuration.ice_servers, - ) - ) - ws_client.subscribe.assert_called_once() - - # Simulate the answer from the go2rtc server - callback = ws_client.subscribe.call_args[0][0] - callback(WebRTCAnswer(ANSWER_SDP)) - receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) - - await test() - - rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] - ) - - # Stream exists but the source is different - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - entity_id: Stream([Producer("rtsp://different")]) - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - rest_client.streams.add.assert_called_once_with( - entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] - ) - - # If the stream is already added, the stream should not be added again. - rest_client.streams.add.reset_mock() - rest_client.streams.list.return_value = { - entity_id: Stream([Producer("rtsp://stream")]) - } - - receive_message_callback.reset_mock() - ws_client.reset_mock() - await test() - - rest_client.streams.add.assert_not_called() - assert isinstance(camera._webrtc_provider, WebRTCProvider) - - # Set stream source to None and provider should be skipped - rest_client.streams.list.return_value = {} - receive_message_callback.reset_mock() - camera.set_stream_source(None) - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - receive_message_callback.assert_called_once_with( - WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") + await _test_setup_and_signaling( + hass, rest_client, ws_client, config, after_setup, init_test_integration ) mock_get_binary.assert_not_called() From 300724443a1780420ce451e3b610876082b6b92d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 11 Nov 2024 17:35:54 +0100 Subject: [PATCH 053/123] Bump spotifyaio to 0.8.8 (#130372) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index afe352904ceb..8f8f7e0d5882 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -9,6 +9,6 @@ "iot_class": "cloud_polling", "loggers": ["spotipy"], "quality_scale": "silver", - "requirements": ["spotifyaio==0.8.7"], + "requirements": ["spotifyaio==0.8.8"], "zeroconf": ["_spotify-connect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 2d1b21855bee..8ff19f5eadfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2707,7 +2707,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.7 +spotifyaio==0.8.8 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 028eba2119ff..c60235d050d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2159,7 +2159,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.7 +spotifyaio==0.8.8 # homeassistant.components.sql sqlparse==0.5.0 From 929164251af6e093c0e5919d4871d8fc213b3923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 11 Nov 2024 21:05:52 +0100 Subject: [PATCH 054/123] Bump Tibber 0.30.8 (#130388) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index d1bfefec4848..bc9304ab59d3 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.30.7"] + "requirements": ["pyTibber==0.30.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8ff19f5eadfa..bb5ed4f629a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.30.7 +pyTibber==0.30.8 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c60235d050d0..55a799bbeff9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ pyElectra==1.2.4 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.30.7 +pyTibber==0.30.8 # homeassistant.components.dlink pyW215==0.7.0 From 79329e16cfd865e29def78932413011e5e3d4224 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Nov 2024 13:48:49 -0600 Subject: [PATCH 055/123] Fix missing title placeholders in powerwall reauth (#130389) --- homeassistant/components/powerwall/config_flow.py | 6 +++++- tests/components/powerwall/test_config_flow.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index bacbff632111..0c39392ca19b 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -251,8 +251,8 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN): """Handle reauth confirmation.""" errors: dict[str, str] | None = {} description_placeholders: dict[str, str] = {} + reauth_entry = self._get_reauth_entry() if user_input is not None: - reauth_entry = self._get_reauth_entry() errors, _, description_placeholders = await self._async_try_connect( {CONF_IP_ADDRESS: reauth_entry.data[CONF_IP_ADDRESS], **user_input} ) @@ -261,6 +261,10 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN): reauth_entry, data_updates=user_input ) + self.context["title_placeholders"] = { + "name": reauth_entry.title, + "ip_address": reauth_entry.data[CONF_IP_ADDRESS], + } return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}), diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 5074a289d191..1ff1470f81c0 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -339,6 +339,11 @@ async def test_form_reauth(hass: HomeAssistant) -> None: result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} + flow = hass.config_entries.flow.async_get(result["flow_id"]) + assert flow["context"]["title_placeholders"] == { + "ip_address": VALID_CONFIG[CONF_IP_ADDRESS], + "name": entry.title, + } mock_powerwall = await _mock_powerwall_site_name(hass, "My site") From 4d3502e061917f295ec393a5187ecfa6837e1a50 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:46:08 +0000 Subject: [PATCH 056/123] Bump ring library ring-doorbell to 0.9.9 (#129966) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 4e0514ba7f9f..63c47cb2979b 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell==0.9.8"] + "requirements": ["ring-doorbell==0.9.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index bb5ed4f629a6..7292990a9d56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2556,7 +2556,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.8 +ring-doorbell==0.9.9 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55a799bbeff9..4d8777b7e6d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2044,7 +2044,7 @@ reolink-aio==0.10.4 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.8 +ring-doorbell==0.9.9 # homeassistant.components.roku rokuecp==0.19.3 From 103a84b4bd1475a16431299b0b032cb3e8e303f8 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:04:23 +0000 Subject: [PATCH 057/123] Bump ring-doorbell to 0.9.12 (#130419) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 63c47cb2979b..e431c6800813 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell==0.9.9"] + "requirements": ["ring-doorbell==0.9.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7292990a9d56..35b07f41e7cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2556,7 +2556,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.9 +ring-doorbell==0.9.12 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d8777b7e6d8..fd66e42abec9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2044,7 +2044,7 @@ reolink-aio==0.10.4 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell==0.9.9 +ring-doorbell==0.9.12 # homeassistant.components.roku rokuecp==0.19.3 From f4798d27c7c240649fb7060ebed795acf1d4799a Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:09:58 +0000 Subject: [PATCH 058/123] Do not trigger events for updated ring events (#130430) --- homeassistant/components/ring/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ring/event.py b/homeassistant/components/ring/event.py index e6d9d25542fa..71a4bc8aea5d 100644 --- a/homeassistant/components/ring/event.py +++ b/homeassistant/components/ring/event.py @@ -96,7 +96,7 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity) @callback def _handle_coordinator_update(self) -> None: - if alert := self._get_coordinator_alert(): + if (alert := self._get_coordinator_alert()) and not alert.is_update: self._async_handle_event(alert.kind) super()._handle_coordinator_update() From 08f6f2759bd40d19e9691139ef313150a8e3fb75 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 07:55:13 +0100 Subject: [PATCH 059/123] Add title to water heater component (#130446) --- homeassistant/components/water_heater/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 741b277d84df..07e132a0b5bb 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -1,4 +1,5 @@ { + "title": "Water heater", "device_automation": { "action_type": { "turn_on": "[%key:common::device_automation::action_type::turn_on%]", @@ -7,7 +8,7 @@ }, "entity_component": { "_": { - "name": "Water heater", + "name": "[%key:component::water_heater::title%]", "state": { "off": "[%key:common::state::off%]", "eco": "Eco", From 8b173656e769649edf559a4f910f74c843118727 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Nov 2024 18:44:32 +0100 Subject: [PATCH 060/123] Fix translation in statistics (#130455) * Fix translation in statistics * Update homeassistant/components/statistics/strings.json --- homeassistant/components/statistics/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json index a060c88da249..3e6fec9d986f 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -23,10 +23,10 @@ "state_characteristic": { "description": "Read the documention for further details on available options and how to use them.", "data": { - "state_characteristic": "State_characteristic" + "state_characteristic": "Statistic characteristic" }, "data_description": { - "state_characteristic": "The characteristic that should be used as the state of the statistics sensor." + "state_characteristic": "The statistic characteristic that should be used as the state of the sensor." } }, "options": { From 28f46a0f8803c6dcbbd2f39073b8b2a8fdc96a08 Mon Sep 17 00:00:00 2001 From: Kelvin Dekker <143089625+KelvinDekker@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:33:52 +0100 Subject: [PATCH 061/123] Fix typo in file strings (#130465) --- homeassistant/components/file/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/file/strings.json b/homeassistant/components/file/strings.json index 60ebf451f78d..8806c67cd967 100644 --- a/homeassistant/components/file/strings.json +++ b/homeassistant/components/file/strings.json @@ -18,7 +18,7 @@ }, "data_description": { "file_path": "The local file path to retrieve the sensor value from", - "value_template": "A template to render the the sensors value based on the file content", + "value_template": "A template to render the sensors value based on the file content", "unit_of_measurement": "Unit of measurement for the sensor" } }, From 0976476d16773498fa1f8a651911758963eb12b2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 07:55:33 +0100 Subject: [PATCH 062/123] Bump aiowithings to 3.1.2 (#130469) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index a0a86be5da38..c24bdb743bfe 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.1.1"] + "requirements": ["aiowithings==3.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35b07f41e7cb..563ddd7e2da3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.1 +aiowithings==3.1.2 # homeassistant.components.yandex_transport aioymaps==1.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd66e42abec9..1e830dc41a82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -396,7 +396,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.1 +aiowithings==3.1.2 # homeassistant.components.yandex_transport aioymaps==1.2.5 From 8a224331680ae5133e260070e50791d646d54922 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Nov 2024 04:13:29 -0500 Subject: [PATCH 063/123] Ensure ZHA setup works with container installs (#130470) --- homeassistant/components/zha/config_flow.py | 36 +++++++++-------- tests/components/zha/test_config_flow.py | 43 ++++++++++++++++----- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 20eb006eb74c..72ff66912d5d 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt as dt_util @@ -104,25 +105,26 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: yellow_radio.description = "Yellow Zigbee module" yellow_radio.manufacturer = "Nabu Casa" - # Present the multi-PAN addon as a setup option, if it's available - multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( - hass - ) - - try: - addon_info = await multipan_manager.async_get_addon_info() - except (AddonError, KeyError): - addon_info = None - - if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: - addon_port = ListPortInfo( - device=silabs_multiprotocol_addon.get_zigbee_socket(), - skip_link_detection=True, + if is_hassio(hass): + # Present the multi-PAN addon as a setup option, if it's available + multipan_manager = ( + await silabs_multiprotocol_addon.get_multiprotocol_addon_manager(hass) ) - addon_port.description = "Multiprotocol add-on" - addon_port.manufacturer = "Nabu Casa" - ports.append(addon_port) + try: + addon_info = await multipan_manager.async_get_addon_info() + except (AddonError, KeyError): + addon_info = None + + if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED: + addon_port = ListPortInfo( + device=silabs_multiprotocol_addon.get_zigbee_socket(), + skip_link_detection=True, + ) + + addon_port.description = "Multiprotocol add-on" + addon_port.manufacturer = "Nabu Casa" + ports.append(addon_port) return ports diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 1382c5c2569e..87ba46a4ced1 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -21,7 +21,7 @@ import zigpy.types from homeassistant import config_entries from homeassistant.components import ssdp, usb, zeroconf -from homeassistant.components.hassio import AddonState +from homeassistant.components.hassio import AddonError, AddonState from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.const import ( @@ -1878,10 +1878,23 @@ async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: ) +async def test_config_flow_ports_no_hassio(hass: HomeAssistant) -> None: + """Test config flow serial port name when this is not a hassio install.""" + + with ( + patch("homeassistant.components.zha.config_flow.is_hassio", return_value=False), + patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), + ): + ports = await config_flow.list_serial_ports(hass) + + assert ports == [] + + async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> None: """Test config flow serial port name for multiprotocol add-on.""" with ( + patch("homeassistant.components.zha.config_flow.is_hassio", return_value=True), patch( "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info" ) as async_get_addon_info, @@ -1889,16 +1902,28 @@ async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> ): async_get_addon_info.return_value.state = AddonState.RUNNING async_get_addon_info.return_value.hostname = "core-silabs-multiprotocol" + ports = await config_flow.list_serial_ports(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - ) + assert len(ports) == 1 + assert ports[0].description == "Multiprotocol add-on" + assert ports[0].manufacturer == "Nabu Casa" + assert ports[0].device == "socket://core-silabs-multiprotocol:9999" - assert ( - result["data_schema"].schema["path"].container[0] - == "socket://core-silabs-multiprotocol:9999 - Multiprotocol add-on - Nabu Casa" - ) + +async def test_config_flow_port_no_multiprotocol(hass: HomeAssistant) -> None: + """Test config flow serial port listing when addon info fails to load.""" + + with ( + patch("homeassistant.components.zha.config_flow.is_hassio", return_value=True), + patch( + "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info", + side_effect=AddonError, + ), + patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), + ): + ports = await config_flow.list_serial_ports(hass) + + assert ports == [] @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) From e4cb3c67d9e5af50f1ece89db780c2b40253db76 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 13 Nov 2024 10:55:28 +0100 Subject: [PATCH 064/123] Fix legacy _attr_state handling in AlarmControlPanel (#130479) --- .../alarm_control_panel/__init__.py | 14 ++- .../alarm_control_panel/test_init.py | 93 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 2946fc64941e..a9e433a36505 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -6,7 +6,7 @@ import asyncio from datetime import timedelta from functools import partial import logging -from typing import Any, Final, final +from typing import TYPE_CHECKING, Any, Final, final from propcache import cached_property import voluptuous as vol @@ -221,9 +221,15 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A @property def state(self) -> str | None: """Return the current state.""" - if (alarm_state := self.alarm_state) is None: - return None - return alarm_state + if (alarm_state := self.alarm_state) is not None: + return alarm_state + if self._attr_state is not None: + # Backwards compatibility for integrations that set state directly + # Should be removed in 2025.11 + if TYPE_CHECKING: + assert isinstance(self._attr_state, str) + return self._attr_state + return None @cached_property def alarm_state(self) -> AlarmControlPanelState | None: diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py index 90b23f87ab12..89a2a2a2b1af 100644 --- a/tests/components/alarm_control_panel/test_init.py +++ b/tests/components/alarm_control_panel/test_init.py @@ -489,3 +489,96 @@ async def test_alarm_control_panel_log_deprecated_state_warning_using_attr_state ) # Test we only log once assert "Entities should implement the 'alarm_state' property and" not in caplog.text + + +async def test_alarm_control_panel_deprecated_state_does_not_break_state( + hass: HomeAssistant, + code_format: CodeFormat | None, + supported_features: AlarmControlPanelEntityFeature, + code_arm_required: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test using _attr_state attribute does not break state.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups( + config_entry, [ALARM_CONTROL_PANEL_DOMAIN] + ) + return True + + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + class MockLegacyAlarmControlPanel(MockAlarmControlPanel): + """Mocked alarm control entity.""" + + def __init__( + self, + supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature( + 0 + ), + code_format: CodeFormat | None = None, + code_arm_required: bool = True, + ) -> None: + """Initialize the alarm control.""" + self._attr_state = "armed_away" + super().__init__(supported_features, code_format, code_arm_required) + + def alarm_disarm(self, code: str | None = None) -> None: + """Mock alarm disarm calls.""" + self._attr_state = "disarmed" + + entity = MockLegacyAlarmControlPanel( + supported_features=supported_features, + code_format=code_format, + code_arm_required=code_arm_required, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test alarm control panel platform via config entry.""" + async_add_entities([entity]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + with patch.object( + MockLegacyAlarmControlPanel, + "__module__", + "tests.custom_components.test.alarm_control_panel", + ): + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == "armed_away" + + with patch.object( + MockLegacyAlarmControlPanel, + "__module__", + "tests.custom_components.test.alarm_control_panel", + ): + await help_test_async_alarm_control_panel_service( + hass, entity.entity_id, SERVICE_ALARM_DISARM + ) + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.state == "disarmed" From 3a2f996c13ef15acde2b6e47f595724daaa281b3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 13 Nov 2024 07:35:51 +0100 Subject: [PATCH 065/123] Bump reolink_aio to 0.11.0 (#130481) --- 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 23a46c5e1c99..22fd625770fa 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.10.4"] + "requirements": ["reolink-aio==0.11.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 563ddd7e2da3..12d8e057facd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2547,7 +2547,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.10.4 +reolink-aio==0.11.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e830dc41a82..4bb5698416c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2038,7 +2038,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.10.4 +reolink-aio==0.11.0 # homeassistant.components.rflink rflink==0.0.66 From 1e3c2c0631935f0e13f76283ff412644c3d178dd Mon Sep 17 00:00:00 2001 From: Sheldon Ip <4224778+sheldonip@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:46:52 -0800 Subject: [PATCH 066/123] Fix translations in subaru (#130486) --- homeassistant/components/subaru/strings.json | 4 ++-- tests/components/subaru/test_config_flow.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 78625192e4a5..00da729dccdc 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -37,13 +37,13 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "incorrect_pin": "Incorrect PIN", "bad_pin_format": "PIN should be 4 digits", - "two_factor_request_failed": "Request for 2FA code failed, please try again", "bad_validation_code_format": "Validation code should be 6 digits", "incorrect_validation_code": "Incorrect validation code" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "two_factor_request_failed": "Request for 2FA code failed, please try again" } }, "options": { diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index d930aafbdfb1..6abc544c92a4 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -192,10 +192,6 @@ async def test_two_factor_request_success( assert len(mock_two_factor_request.mock_calls) == 1 -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.subaru.config.abort.two_factor_request_failed"], -) async def test_two_factor_request_fail( hass: HomeAssistant, two_factor_start_form ) -> None: From 433e3718f8b392f718ffd28f1249514e53749456 Mon Sep 17 00:00:00 2001 From: Tony <29752086+ms264556@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:43:59 +1300 Subject: [PATCH 067/123] Bump aioruckus to 0.42 (#130487) --- homeassistant/components/ruckus_unleashed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index 2066b65221e5..8d56f3a55633 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioruckus"], - "requirements": ["aioruckus==0.41"] + "requirements": ["aioruckus==0.42"] } diff --git a/requirements_all.txt b/requirements_all.txt index 12d8e057facd..01ebfb93c588 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,7 +354,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.41 +aioruckus==0.42 # homeassistant.components.russound_rio aiorussound==4.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bb5698416c8..8731011a111f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -336,7 +336,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.41 +aioruckus==0.42 # homeassistant.components.russound_rio aiorussound==4.0.5 From 4b13d8bc47ead0c7d375ecfd591362f7e91b144b Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 13 Nov 2024 09:55:52 +0100 Subject: [PATCH 068/123] Bump go2rtc-client to 0.1.1 (#130498) --- homeassistant/components/go2rtc/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/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index ea9308e5e182..201b7168847a 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/go2rtc", "integration_type": "system", "iot_class": "local_polling", - "requirements": ["go2rtc-client==0.1.0"], + "requirements": ["go2rtc-client==0.1.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5b7e1b4e837f..caa30fa24d16 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ ciso8601==2.3.1 cryptography==43.0.1 dbus-fast==2.24.3 fnv-hash-fast==1.0.2 -go2rtc-client==0.1.0 +go2rtc-client==0.1.1 ha-av==10.1.1 ha-ffmpeg==3.2.2 habluetooth==3.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 01ebfb93c588..34f0fcf9b2dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.0 +go2rtc-client==0.1.1 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8731011a111f..ba86cd4ecf9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -836,7 +836,7 @@ gios==5.0.0 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.0 +go2rtc-client==0.1.1 # homeassistant.components.goalzero goalzero==0.2.2 From 4c24e26926549900ab6ebe500446d0e9ec549e62 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Nov 2024 12:21:15 +0100 Subject: [PATCH 069/123] Bump aiowithings to 3.1.3 (#130504) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index c24bdb743bfe..f9e8328ae53c 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.1.2"] + "requirements": ["aiowithings==3.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34f0fcf9b2dc..dbb04a575694 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.2 +aiowithings==3.1.3 # homeassistant.components.yandex_transport aioymaps==1.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba86cd4ecf9e..aa988ff7286b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -396,7 +396,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.2 # homeassistant.components.withings -aiowithings==3.1.2 +aiowithings==3.1.3 # homeassistant.components.yandex_transport aioymaps==1.2.5 From cb104935eaddb4be5b090fac6d903ee9f1bd9ef5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 13 Nov 2024 11:01:05 +0100 Subject: [PATCH 070/123] Add go2rtc recommended version (#130508) --- .pre-commit-config.yaml | 2 +- homeassistant/components/go2rtc/__init__.py | 31 ++++++++++-- homeassistant/components/go2rtc/const.py | 1 + homeassistant/components/go2rtc/strings.json | 8 +++ script/hassfest/docker.py | 5 +- tests/components/go2rtc/conftest.py | 6 ++- tests/components/go2rtc/test_init.py | 52 ++++++++++++++++++-- 7 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/go2rtc/strings.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a619936cbbf3..f7072e5c96ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,7 +90,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$ + files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$ - id: hassfest-mypy-config name: hassfest-mypy-config entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index fc91ef5e546e..f1f6e44abc19 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -4,6 +4,7 @@ import logging import shutil from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError +from awesomeversion import AwesomeVersion from go2rtc_client import Go2RtcRestClient from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.ws import ( @@ -32,13 +33,23 @@ from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery_flow +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + issue_registry as ir, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from homeassistant.util.package import is_docker_env -from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL +from .const import ( + CONF_DEBUG_UI, + DEBUG_UI_URL_MESSAGE, + DOMAIN, + HA_MANAGED_URL, + RECOMMENDED_VERSION, +) from .server import Server _LOGGER = logging.getLogger(__name__) @@ -147,7 +158,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Validate the server URL try: client = Go2RtcRestClient(async_get_clientsession(hass), url) - await client.validate_server_version() + version = await client.validate_server_version() + if version < AwesomeVersion(RECOMMENDED_VERSION): + ir.async_create_issue( + hass, + DOMAIN, + "recommended_version", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="recommended_version", + translation_placeholders={ + "recommended_version": RECOMMENDED_VERSION, + "current_version": str(version), + }, + ) except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index d33ae3e38975..3c1c84c42b56 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,3 +6,4 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" +RECOMMENDED_VERSION = "1.9.7" diff --git a/homeassistant/components/go2rtc/strings.json b/homeassistant/components/go2rtc/strings.json new file mode 100644 index 000000000000..e350c19af96c --- /dev/null +++ b/homeassistant/components/go2rtc/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "recommended_version": { + "title": "Outdated go2rtc server detected", + "description": "We detected that you are using an outdated go2rtc server version. For the best experience, we recommend updating the go2rtc server to version `{recommended_version}`.\nCurrently you are using version `{current_version}`." + } + } +} diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 9d38d8f71282..137bbc7ff66c 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from pathlib import Path from homeassistant import core +from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION as GO2RTC_VERSION from homeassistant.const import Platform from homeassistant.util import executor, thread from script.gen_requirements_all import gather_recursive_requirements @@ -112,8 +113,6 @@ LABEL "com.github.actions.icon"="terminal" LABEL "com.github.actions.color"="gray-dark" """ -_GO2RTC_VERSION = "1.9.7" - def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]: package_versions: dict[str, str] = {} @@ -197,7 +196,7 @@ def _generate_files(config: Config) -> list[File]: DOCKERFILE_TEMPLATE.format( timeout=timeout, **package_versions, - go2rtc=_GO2RTC_VERSION, + go2rtc=GO2RTC_VERSION, ), config.root / "Dockerfile", ), diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index 42b363b23244..abb139b89bfa 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -3,9 +3,11 @@ from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch +from awesomeversion import AwesomeVersion from go2rtc_client.rest import _StreamClient, _WebRTCClient import pytest +from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION from homeassistant.components.go2rtc.server import Server GO2RTC_PATH = "homeassistant.components.go2rtc" @@ -23,7 +25,9 @@ def rest_client() -> Generator[AsyncMock]: client = mock_client.return_value client.streams = streams = Mock(spec_set=_StreamClient) streams.list.return_value = {} - client.validate_server_version = AsyncMock() + client.validate_server_version = AsyncMock( + return_value=AwesomeVersion(RECOMMENDED_VERSION) + ) client.webrtc = Mock(spec_set=_WebRTCClient) yield client diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 9388110366ec..0f1cac6942da 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -6,6 +6,7 @@ from typing import NamedTuple from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError +from awesomeversion import AwesomeVersion from go2rtc_client import Stream from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.models import Producer @@ -36,10 +37,12 @@ from homeassistant.components.go2rtc.const import ( CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, + RECOMMENDED_VERSION, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -199,6 +202,7 @@ async def init_test_integration( async def _test_setup_and_signaling( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, config: ConfigType, @@ -211,6 +215,7 @@ async def _test_setup_and_signaling( assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done(wait_background_tasks=True) + assert issue_registry.async_get_issue(DOMAIN, "recommended_version") is None config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].state == ConfigEntryState.LOADED @@ -306,6 +311,7 @@ async def _test_setup_and_signaling( @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) async def test_setup_go_binary( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: AsyncMock, @@ -324,7 +330,13 @@ async def test_setup_go_binary( server_start.assert_called_once() await _test_setup_and_signaling( - hass, rest_client, ws_client, config, after_setup, init_test_integration + hass, + issue_registry, + rest_client, + ws_client, + config, + after_setup, + init_test_integration, ) await hass.async_stop() @@ -340,8 +352,9 @@ async def test_setup_go_binary( ], ) @pytest.mark.parametrize("has_go2rtc_entry", [True, False]) -async def test_setup_go( +async def test_setup( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, rest_client: AsyncMock, ws_client: Mock, server: Mock, @@ -359,7 +372,13 @@ async def test_setup_go( server.assert_not_called() await _test_setup_and_signaling( - hass, rest_client, ws_client, config, after_setup, init_test_integration + hass, + issue_registry, + rest_client, + ws_client, + config, + after_setup, + init_test_integration, ) mock_get_binary.assert_not_called() @@ -711,3 +730,30 @@ async def test_config_entry_remove(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert not await hass.config_entries.async_setup(config_entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + +@pytest.mark.parametrize("config", [{DOMAIN: {CONF_URL: "http://localhost:1984"}}]) +@pytest.mark.usefixtures("server") +async def test_setup_with_recommended_version_repair( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + rest_client: AsyncMock, + config: ConfigType, +) -> None: + """Test setup integration entry fails.""" + rest_client.validate_server_version.return_value = AwesomeVersion("1.9.5") + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + + # Verify the issue is created + issue = issue_registry.async_get_issue(DOMAIN, "recommended_version") + assert issue + assert issue.is_fixable is False + assert issue.is_persistent is False + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.issue_id == "recommended_version" + assert issue.translation_key == "recommended_version" + assert issue.translation_placeholders == { + "recommended_version": RECOMMENDED_VERSION, + "current_version": "1.9.5", + } From 876b86cd3d314bcbd8ae4c03ded9574f05db78ce Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Wed, 13 Nov 2024 10:42:26 -0700 Subject: [PATCH 071/123] fix translation in srp_energy (#130540) --- homeassistant/components/srp_energy/strings.json | 3 ++- tests/components/srp_energy/test_config_flow.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json index 191d10a70dd6..eca4f4654351 100644 --- a/homeassistant/components/srp_energy/strings.json +++ b/homeassistant/components/srp_energy/strings.json @@ -17,7 +17,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "unknown": "Unexpected error" } }, "entity": { diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 149e08014ac3..e3abb3c98df2 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -100,10 +100,6 @@ async def test_form_invalid_auth( assert result["errors"] == {"base": "invalid_auth"} -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.srp_energy.config.abort.unknown"], -) async def test_form_unknown_error( hass: HomeAssistant, mock_srp_energy_config_flow: MagicMock, From 53e38454b22fa06304aaadd6a6a7318a7986b39f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Nov 2024 02:45:08 -0600 Subject: [PATCH 072/123] Fix non-thread-safe operation in powerview number (#130557) --- homeassistant/components/hunterdouglas_powerview/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py index f893b04b2d1e..fb8c9f76d791 100644 --- a/homeassistant/components/hunterdouglas_powerview/number.py +++ b/homeassistant/components/hunterdouglas_powerview/number.py @@ -95,7 +95,7 @@ class PowerViewNumber(ShadeEntity, RestoreNumber): self.entity_description = description self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" - def set_native_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._attr_native_value = value self.entity_description.store_value_fn(self.coordinator, self._shade.id, value) From 95d60987ab99468a0cc5957968e202c8edb697f1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 15 Nov 2024 03:55:22 -0500 Subject: [PATCH 073/123] Bump ZHA dependencies (#130563) --- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/update.py | 23 +++++++++++----------- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/test_update.py | 2 +- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 96c9bc030f67..8736dc895494 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.24", "zha==0.0.37"], + "requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.39"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 151d1c495e87..18b8ed1cca5f 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -4,7 +4,6 @@ from __future__ import annotations import functools import logging -import math from typing import Any from zha.exceptions import ZHAException @@ -97,6 +96,7 @@ class ZHAFirmwareUpdateEntity( | UpdateEntityFeature.SPECIFIC_VERSION | UpdateEntityFeature.RELEASE_NOTES ) + _attr_display_precision = 2 # 40 byte chunks with ~200KB files increments by 0.02% def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: """Initialize the ZHA siren.""" @@ -115,20 +115,19 @@ class ZHAFirmwareUpdateEntity( def in_progress(self) -> bool | int | None: """Update installation progress. + Should return a boolean (True if in progress, False if not). + """ + return self.entity_data.entity.in_progress + + @property + def update_percentage(self) -> int | float | None: + """Update installation progress. + Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used. - Can either return a boolean (True if in progress, False if not) - or an integer to indicate the progress in from 0 to 100%. + Can either return a number to indicate the progress from 0 to 100% or None. """ - if not self.entity_data.entity.in_progress: - return self.entity_data.entity.in_progress - - # Stay in an indeterminate state until we actually send something - if self.entity_data.entity.progress == 0: - return True - - # Rescale 0-100% to 2-100% to avoid 0 and 1 colliding with None, False, and True - return int(math.ceil(2 + 98 * self.entity_data.entity.progress / 100)) + return self.entity_data.entity.update_percentage @property def latest_version(self) -> str | None: diff --git a/requirements_all.txt b/requirements_all.txt index dbb04a575694..4b3777efe85c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2900,7 +2900,7 @@ unifi_ap==0.0.1 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.24 +universal-silabs-flasher==0.0.25 # homeassistant.components.upb upb-lib==0.5.8 @@ -3066,7 +3066,7 @@ zeroconf==0.136.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.37 +zha==0.0.39 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa988ff7286b..2eb0357558a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2307,7 +2307,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.2.0 # homeassistant.components.zha -universal-silabs-flasher==0.0.24 +universal-silabs-flasher==0.0.25 # homeassistant.components.upb upb-lib==0.5.8 @@ -2449,7 +2449,7 @@ zeroconf==0.136.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.37 +zha==0.0.39 # homeassistant.components.zwave_js zwave-js-server-python==0.58.1 diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 4b6dff4fc6b8..cd48ae62ff3b 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -394,7 +394,7 @@ async def test_firmware_update_success( attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" ) assert attrs[ATTR_IN_PROGRESS] is True - assert attrs[ATTR_UPDATE_PERCENTAGE] == 58 + assert attrs[ATTR_UPDATE_PERCENTAGE] == pytest.approx(100 * 40 / 70) assert ( attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" From 1bc005d0d4b7bab42bfaf34f0f8b23866f75fb6f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 14 Nov 2024 10:35:03 +0100 Subject: [PATCH 074/123] Update uptime deviation for Vodafone Station (#130571) Update sensor.py --- homeassistant/components/vodafone_station/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index fb76253eb3d1..307fcaf0ea8e 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -22,7 +22,7 @@ from .const import _LOGGER, DOMAIN, LINE_TYPES from .coordinator import VodafoneStationRouter NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"] -UPTIME_DEVIATION = 45 +UPTIME_DEVIATION = 60 @dataclass(frozen=True, kw_only=True) From 281a8eda31db300f8c11e11778f9f58936cfbe3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Fri, 15 Nov 2024 09:46:12 +0100 Subject: [PATCH 075/123] Fixes webhook schema for different temp and volume units (#130578) --- homeassistant/components/plaato/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 59441f250251..585b6ecfd82e 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -64,10 +64,10 @@ WEBHOOK_SCHEMA = vol.Schema( vol.Required(ATTR_DEVICE_NAME): cv.string, vol.Required(ATTR_DEVICE_ID): cv.positive_int, vol.Required(ATTR_TEMP_UNIT): vol.In( - UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + [UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT] ), vol.Required(ATTR_VOLUME_UNIT): vol.In( - UnitOfVolume.LITERS, UnitOfVolume.GALLONS + [UnitOfVolume.LITERS, UnitOfVolume.GALLONS] ), vol.Required(ATTR_BPM): cv.positive_int, vol.Required(ATTR_TEMP): vol.Coerce(float), From 6d861e7f47d8d79e71753d45d1d24eab88551737 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 14 Nov 2024 10:48:25 +0100 Subject: [PATCH 076/123] Bump reolink-aio to 0.11.1 (#130600) --- 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 22fd625770fa..7921bdb6ed58 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.11.0"] + "requirements": ["reolink-aio==0.11.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4b3777efe85c..1e332154b39b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2547,7 +2547,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.0 +reolink-aio==0.11.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2eb0357558a5..1e6d7e360090 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2038,7 +2038,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.0 +reolink-aio==0.11.1 # homeassistant.components.rflink rflink==0.0.66 From 5acdf5897635d764b2f7b5d03026ee5072a3738a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 14 Nov 2024 11:01:26 +0100 Subject: [PATCH 077/123] Fix hassfest by adding go2rtc reqs (#130602) --- script/hassfest/docker.py | 2 ++ script/hassfest/docker/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 137bbc7ff66c..0eb72b91c023 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -161,6 +161,8 @@ def _generate_hassfest_dockerimage( packages.update( gather_recursive_requirements(platform.value, already_checked_domains) ) + # Add go2rtc requirements as this file needs the go2rtc integration + packages.update(gather_recursive_requirements("go2rtc", already_checked_domains)) return File( _HASSFEST_TEMPLATE.format( diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 26db1fe5a1a5..3128b0f3bbdd 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.4.28,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.1 \ - PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From e478b9b599579328c7b066bcc0573a7b62b03b14 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:17:08 +0100 Subject: [PATCH 078/123] Add missing translation string to smarty (#130624) --- homeassistant/components/smarty/strings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/smarty/strings.json b/homeassistant/components/smarty/strings.json index 367a3a346252..37a6c5cbca12 100644 --- a/homeassistant/components/smarty/strings.json +++ b/homeassistant/components/smarty/strings.json @@ -28,6 +28,10 @@ "deprecated_yaml_import_issue_auth_error": { "title": "YAML import failed due to an authentication error", "description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + }, + "deprecated_yaml_import_issue_cannot_connect": { + "title": "YAML import failed due to a connection error", + "description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." } }, "entity": { From 8b9c4db2b32978d277cf47f1794f486d9aa3394b Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 14 Nov 2024 10:50:34 -0500 Subject: [PATCH 079/123] Bump sense-energy to 0.13.4 (#130625) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index d4889c0c5f5b..da3912a9d258 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.13.3"] + "requirements": ["sense-energy==0.13.4"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index df2317c3a6c9..966488b6a487 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.13.3"] + "requirements": ["sense-energy==0.13.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e332154b39b..9a71b7df2d86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2623,7 +2623,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.3 +sense-energy==0.13.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e6d7e360090..412d55660666 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2090,7 +2090,7 @@ securetar==2024.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.3 +sense-energy==0.13.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 663ebe199d38a21fc134cdf79655ef07d66970bc Mon Sep 17 00:00:00 2001 From: Alistair Galbraith Date: Fri, 15 Nov 2024 07:08:43 -0800 Subject: [PATCH 080/123] Fix scene loading issue (#130627) --- homeassistant/components/hue/scene.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 6808ddb53530..1d83804820d9 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -130,10 +130,15 @@ class HueSceneEntity(HueSceneEntityBase): @property def is_dynamic(self) -> bool: """Return if this scene has a dynamic color palette.""" - if self.resource.palette.color and len(self.resource.palette.color) > 1: + if ( + self.resource.palette + and self.resource.palette.color + and len(self.resource.palette.color) > 1 + ): return True if ( - self.resource.palette.color_temperature + self.resource.palette + and self.resource.palette.color_temperature and len(self.resource.palette.color_temperature) > 1 ): return True From 6d561ca373e5bcd39bd997e87c1c48fb67c052f4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:39:14 +0100 Subject: [PATCH 081/123] Add missing translation string to hvv_departures (#130634) --- homeassistant/components/hvv_departures/strings.json | 4 ++++ tests/components/hvv_departures/test_config_flow.py | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hvv_departures/strings.json b/homeassistant/components/hvv_departures/strings.json index a9ec58f12ad5..f69dcd220471 100644 --- a/homeassistant/components/hvv_departures/strings.json +++ b/homeassistant/components/hvv_departures/strings.json @@ -32,6 +32,10 @@ } }, "options": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, "step": { "init": { "title": "Options", diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 8d82382d9a29..c85bfb7f6ee9 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -4,7 +4,6 @@ import json from unittest.mock import patch from pygti.exceptions import CannotConnect, InvalidAuth -import pytest from homeassistant.components.hvv_departures.const import ( CONF_FILTER, @@ -313,10 +312,6 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.hvv_departures.options.error.invalid_auth"], -) async def test_options_flow_invalid_auth(hass: HomeAssistant) -> None: """Test that options flow works.""" @@ -360,10 +355,6 @@ async def test_options_flow_invalid_auth(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "invalid_auth"} -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.hvv_departures.options.error.cannot_connect"], -) async def test_options_flow_cannot_connect(hass: HomeAssistant) -> None: """Test that options flow works.""" From 5f09eb97e1ddb5cd7a4741e52992ec1569d58231 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:40:01 +0100 Subject: [PATCH 082/123] Add missing translation string to lg_netcast (#130635) --- homeassistant/components/lg_netcast/strings.json | 3 ++- tests/components/lg_netcast/test_config_flow.py | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lg_netcast/strings.json b/homeassistant/components/lg_netcast/strings.json index 209c3837261d..0377d4bf3181 100644 --- a/homeassistant/components/lg_netcast/strings.json +++ b/homeassistant/components/lg_netcast/strings.json @@ -25,7 +25,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]" } }, "device_automation": { diff --git a/tests/components/lg_netcast/test_config_flow.py b/tests/components/lg_netcast/test_config_flow.py index 7959c0c445ec..027075824840 100644 --- a/tests/components/lg_netcast/test_config_flow.py +++ b/tests/components/lg_netcast/test_config_flow.py @@ -3,8 +3,6 @@ from datetime import timedelta from unittest.mock import DEFAULT, patch -import pytest - from homeassistant import data_entry_flow from homeassistant.components.lg_netcast.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -114,10 +112,6 @@ async def test_manual_host_unsuccessful_details_response(hass: HomeAssistant) -> assert result["reason"] == "cannot_connect" -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.lg_netcast.config.abort.invalid_host"], -) async def test_manual_host_no_unique_id_response(hass: HomeAssistant) -> None: """Test manual host configuration.""" with _patch_lg_netcast(no_unique_id=True): From 0d695c843f2140b6cc18c1c7a2bdd249d5525529 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:40:38 +0100 Subject: [PATCH 083/123] Add missing translation string to philips_js (#130637) --- homeassistant/components/philips_js/strings.json | 2 +- tests/components/philips_js/test_config_flow.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json index 3ea632ce436d..1f187d89ddac 100644 --- a/homeassistant/components/philips_js/strings.json +++ b/homeassistant/components/philips_js/strings.json @@ -18,11 +18,11 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "pairing_failure": "Unable to pair: {error_id}", "invalid_pin": "Invalid PIN" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "pairing_failure": "Unable to pair: {error_id}", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index c08885634dbf..80d059618133 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -161,10 +161,6 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.philips_js.config.abort.pairing_failure"], -) async def test_pair_request_failed( hass: HomeAssistant, mock_tv_pairable, mock_setup_entry ) -> None: @@ -192,10 +188,6 @@ async def test_pair_request_failed( } -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.philips_js.config.abort.pairing_failure"], -) async def test_pair_grant_failed( hass: HomeAssistant, mock_tv_pairable, mock_setup_entry ) -> None: From f406ffa75ac808e68064cb24a39cad9ff6c9ece2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Fri, 15 Nov 2024 09:41:35 +0100 Subject: [PATCH 084/123] Bump pyplaato to 0.0.19 (#130641) Bumps version of pyplaato to 0.0.19 --- homeassistant/components/plaato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index aac7ec2d06fc..1547501ac506 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/plaato", "iot_class": "cloud_push", "loggers": ["pyplaato"], - "requirements": ["pyplaato==0.0.18"] + "requirements": ["pyplaato==0.0.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a71b7df2d86..eef3cae8a3d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2155,7 +2155,7 @@ pypck==0.7.24 pypjlink2==1.2.1 # homeassistant.components.plaato -pyplaato==0.0.18 +pyplaato==0.0.19 # homeassistant.components.point pypoint==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 412d55660666..1522df976df7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1739,7 +1739,7 @@ pypck==0.7.24 pypjlink2==1.2.1 # homeassistant.components.plaato -pyplaato==0.0.18 +pyplaato==0.0.19 # homeassistant.components.point pypoint==3.0.0 From 880f28e28a7055b786d1465d6b3f6a9f87169184 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 14 Nov 2024 23:09:16 +0100 Subject: [PATCH 085/123] Remove dumping config entry to log in setup of roborock (#130648) --- homeassistant/components/roborock/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index d1cbccc6b057..d02dddece42f 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -47,7 +47,6 @@ class RoborockCoordinators: async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool: """Set up roborock from a config entry.""" - _LOGGER.debug("Integration async setup entry: %s", entry.as_dict()) entry.async_on_unload(entry.add_update_listener(update_listener)) user_data = UserData.from_dict(entry.data[CONF_USER_DATA]) From 942830505a24b1e956707f513d1552f74cc1aad7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 01:50:47 +0100 Subject: [PATCH 086/123] Fix missing translations in vilfo (#130650) --- homeassistant/components/vilfo/config_flow.py | 2 +- homeassistant/components/vilfo/strings.json | 1 + tests/components/vilfo/test_config_flow.py | 6 +----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py index a6cff506f79e..cdba7f1b8c2d 100644 --- a/homeassistant/components/vilfo/config_flow.py +++ b/homeassistant/components/vilfo/config_flow.py @@ -109,7 +109,7 @@ class DomainConfigFlow(ConfigFlow, domain=DOMAIN): try: info = await validate_input(self.hass, user_input) except InvalidHost: - errors[CONF_HOST] = "wrong_host" + errors["base"] = "invalid_host" except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json index f2c4c38780b4..55c996d4a3de 100644 --- a/homeassistant/components/vilfo/strings.json +++ b/homeassistant/components/vilfo/strings.json @@ -14,6 +14,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 24739f509e4d..dcfdc8a9ffa4 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -150,10 +150,6 @@ async def test_form_exceptions( assert result["type"] is FlowResultType.CREATE_ENTRY -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.vilfo.config.error.wrong_host"], -) async def test_form_wrong_host( hass: HomeAssistant, mock_is_valid_host: AsyncMock, @@ -169,7 +165,7 @@ async def test_form_wrong_host( }, ) - assert result["errors"] == {"host": "wrong_host"} + assert result["errors"] == {"base": "invalid_host"} async def test_form_already_configured( From c6931d656e00f05a5facfe6122f0763988d9514c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:42:01 +0100 Subject: [PATCH 087/123] Fix missing translations in utility_meter (#130652) --- homeassistant/components/utility_meter/strings.json | 3 +++ tests/components/utility_meter/test_config_flow.py | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json index fc1c727fb0a8..e05789aece16 100644 --- a/homeassistant/components/utility_meter/strings.json +++ b/homeassistant/components/utility_meter/strings.json @@ -25,6 +25,9 @@ "tariffs": "A list of supported tariffs, leave empty if only a single tariff is needed." } } + }, + "error": { + "tariffs_not_unique": "Tariffs must be unique" } }, "options": { diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 612bfaa88d7a..560566d7c491 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -72,10 +72,6 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: assert config_entry.title == "Electricity meter" -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.utility_meter.config.error.tariffs_not_unique"], -) async def test_tariffs(hass: HomeAssistant) -> None: """Test tariffs.""" input_sensor_entity_id = "sensor.input" From 1cabcdf2573e907287901705c4775994af4a4270 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:42:15 +0100 Subject: [PATCH 088/123] Fix missing translations in tradfri (#130654) * Fix missing translations in tradfri * Simplify --- homeassistant/components/tradfri/config_flow.py | 5 +---- homeassistant/components/tradfri/strings.json | 2 +- tests/components/tradfri/test_config_flow.py | 6 +----- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 8de401403391..d9911472a672 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -60,10 +60,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): return await self._entry_from_data(auth) except AuthError as err: - if err.code == "invalid_security_code": - errors[KEY_SECURITY_CODE] = err.code - else: - errors["base"] = err.code + errors["base"] = err.code else: user_input = {} diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 69a28a567ab0..9ed7e167e715 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -14,7 +14,7 @@ } }, "error": { - "invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.", + "invalid_security_code": "Failed to register with provided key. If this keeps happening, try restarting the gateway.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout": "Timeout validating the code.", "cannot_authenticate": "Cannot authenticate, is Gateway paired with another server like e.g. Homekit?" diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 5c06851782cc..b6f38b1d83d7 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -86,10 +86,6 @@ async def test_user_connection_timeout( assert result["errors"] == {"base": "timeout"} -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.tradfri.config.error.invalid_security_code"], -) async def test_user_connection_bad_key( hass: HomeAssistant, mock_auth, mock_entry_setup ) -> None: @@ -107,7 +103,7 @@ async def test_user_connection_bad_key( assert len(mock_entry_setup.mock_calls) == 0 assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"security_code": "invalid_security_code"} + assert result["errors"] == {"base": "invalid_security_code"} async def test_discovery_connection( From f74bfdc9743bbca9675aac1c6a66be814c0758e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 01:50:09 +0100 Subject: [PATCH 089/123] Fix missing translations in toon (#130655) --- homeassistant/components/toon/strings.json | 1 + tests/components/toon/test_config_flow.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index ed29e77a58cc..3072896653df 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -16,6 +16,7 @@ "already_configured": "The selected agreement is already configured.", "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "no_agreements": "This account has no Toon displays.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 7855379db5b4..1ad5ea1ca3da 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -249,10 +249,6 @@ async def test_agreement_already_set_up( assert result3["reason"] == "already_configured" -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.toon.config.abort.connection_error"], -) @pytest.mark.usefixtures("current_request_with_host") async def test_toon_abort( hass: HomeAssistant, From 9e4d26137e17159498c374467854f77592816127 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:41:15 +0100 Subject: [PATCH 090/123] Fix missing translations in madvr (#130656) --- homeassistant/components/madvr/strings.json | 6 +++--- tests/components/madvr/test_config_flow.py | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/madvr/strings.json b/homeassistant/components/madvr/strings.json index 06851efa2c8d..1a4f0f79aae4 100644 --- a/homeassistant/components/madvr/strings.json +++ b/homeassistant/components/madvr/strings.json @@ -28,12 +28,12 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "set_up_new_device": "A new device was detected. Please set it up as a new entity instead of reconfiguring." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "no_mac": "A MAC address was not found. It required to identify the device. Please ensure your device is connectable.", - "set_up_new_device": "A new device was detected. Please set it up as a new entity instead of reconfiguring." + "no_mac": "A MAC address was not found. It required to identify the device. Please ensure your device is connectable." } }, "entity": { diff --git a/tests/components/madvr/test_config_flow.py b/tests/components/madvr/test_config_flow.py index 35db8a01b5bf..7b31ec6c17c5 100644 --- a/tests/components/madvr/test_config_flow.py +++ b/tests/components/madvr/test_config_flow.py @@ -165,10 +165,6 @@ async def test_reconfigure_flow( mock_madvr_client.async_cancel_tasks.assert_called() -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.madvr.config.abort.set_up_new_device"], -) async def test_reconfigure_new_device( hass: HomeAssistant, mock_madvr_client: AsyncMock, From 8d6f2e78f5bd57990ad2ccaf5a3aa5a057da503f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:48:02 +0100 Subject: [PATCH 091/123] Fix missing translations in generic (#130672) --- homeassistant/components/generic/config_flow.py | 6 +++++- homeassistant/components/generic/strings.json | 1 + tests/components/generic/test_config_flow.py | 7 ++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 7b10cdfb64b9..a8f3f6f386b9 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -282,7 +282,7 @@ async def async_test_stream( return {CONF_STREAM_SOURCE: "timeout"} await stream.stop() except StreamWorkerError as err: - return {CONF_STREAM_SOURCE: str(err)} + return {CONF_STREAM_SOURCE: "unknown_with_details", "error_details": str(err)} except PermissionError: return {CONF_STREAM_SOURCE: "stream_not_permitted"} except OSError as err: @@ -339,6 +339,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the start of the config flow.""" errors = {} + description_placeholders = {} hass = self.hass if user_input: # Secondary validation because serialised vol can't seem to handle this complexity: @@ -372,6 +373,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): # temporary preview for user to check the image self.preview_cam = user_input return await self.async_step_user_confirm_still() + if "error_details" in errors: + description_placeholders["error"] = errors.pop("error_details") elif self.user_input: user_input = self.user_input else: @@ -379,6 +382,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=build_schema(user_input), + description_placeholders=description_placeholders, errors=errors, ) diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index b05f17efc8d0..94360a5b7c29 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -3,6 +3,7 @@ "config": { "error": { "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_with_details": "An unknown error occurred: {error}", "already_exists": "A camera with these URL settings already exists.", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "unable_still_load_auth": "Unable to load valid image from still image URL: The camera may require a user name and password, or they are not correct.", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 7575a0786758..a882ca4cd8df 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -637,10 +637,6 @@ async def test_form_stream_other_error(hass: HomeAssistant, user_flow) -> None: await hass.async_block_till_done() -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.generic.config.error.Some message"], -) @respx.mock @pytest.mark.usefixtures("fakeimg_png") async def test_form_stream_worker_error( @@ -656,7 +652,8 @@ async def test_form_stream_worker_error( TESTDATA, ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"stream_source": "Some message"} + assert result2["errors"] == {"stream_source": "unknown_with_details"} + assert result2["description_placeholders"] == {"error": "Some message"} @respx.mock From 045e285bfebc1a731e3e0ce6e20e38278d326503 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:40:57 +0100 Subject: [PATCH 092/123] Fix missing translations in onewire (#130673) --- homeassistant/components/onewire/config_flow.py | 2 +- homeassistant/components/onewire/strings.json | 3 +++ tests/components/onewire/test_config_flow.py | 6 +----- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index a217674e3b42..ab8b44f895df 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -137,7 +137,7 @@ class OnewireOptionsFlowHandler(OptionsFlowWithConfigEntry): } if not self.configurable_devices: - return self.async_abort(reason="No configurable devices found.") + return self.async_abort(reason="no_configurable_devices") return await self.async_step_device_selection(user_input=None) diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 8dbcbdf8978f..68585c3203f7 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -94,6 +94,9 @@ } }, "options": { + "abort": { + "no_configurable_devices": "No configurable devices found" + }, "error": { "device_not_selected": "Select devices to configure" }, diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index c554624267da..029e1278c868 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -253,10 +253,6 @@ async def test_user_options_set_multiple( ) -@pytest.mark.parametrize( # Remove when translations fixed - "ignore_translations", - ["component.onewire.options.abort.No configurable devices found."], -) async def test_user_options_no_devices( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: @@ -267,4 +263,4 @@ async def test_user_options_no_devices( result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "No configurable devices found." + assert result["reason"] == "no_configurable_devices" From ca40b96a898c97d53c8724e3765e89df2182b6f6 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Fri, 15 Nov 2024 01:36:24 -0800 Subject: [PATCH 093/123] Bump python-smarttub to 0.0.38 (#130679) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index f2514063a409..432f6338d9f9 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["smarttub"], "quality_scale": "platinum", - "requirements": ["python-smarttub==0.0.36"] + "requirements": ["python-smarttub==0.0.38"] } diff --git a/requirements_all.txt b/requirements_all.txt index eef3cae8a3d3..9164882565f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2396,7 +2396,7 @@ python-ripple-api==0.0.3 python-roborock==2.7.2 # homeassistant.components.smarttub -python-smarttub==0.0.36 +python-smarttub==0.0.38 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1522df976df7..2540db8e09cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1917,7 +1917,7 @@ python-rabbitair==0.0.8 python-roborock==2.7.2 # homeassistant.components.smarttub -python-smarttub==0.0.36 +python-smarttub==0.0.38 # homeassistant.components.songpal python-songpal==0.16.2 From ac270e19bee80462717e405577d310b836626231 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Nov 2024 19:35:42 +0100 Subject: [PATCH 094/123] Bump version to 2024.11.2 --- 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 210e1cfedfeb..a2068f71ecf6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 11 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 8699a1a627f0..e13d9ce2739f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.11.1" +version = "2024.11.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4863243f5a2c5ef994d0bdaa94b52c9beb5aa6c1 Mon Sep 17 00:00:00 2001 From: Davin Kevin Date: Thu, 21 Nov 2024 20:50:49 +0100 Subject: [PATCH 095/123] Prevent endless loop in recorder when using a filter and there are no more states to purge (#126149) Co-authored-by: J. Nick Koston --- homeassistant/components/recorder/purge.py | 16 +- tests/components/recorder/test_purge.py | 165 +++++++++++++++++++++ 2 files changed, 174 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index d28e7e2a5475..329f48e5455c 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -110,7 +110,7 @@ def purge_old_data( _LOGGER.debug("Purging hasn't fully completed yet") return False - if apply_filter and _purge_filtered_data(instance, session) is False: + if apply_filter and not _purge_filtered_data(instance, session): _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") return False @@ -631,7 +631,10 @@ def _purge_old_entity_ids(instance: Recorder, session: Session) -> None: def _purge_filtered_data(instance: Recorder, session: Session) -> bool: - """Remove filtered states and events that shouldn't be in the database.""" + """Remove filtered states and events that shouldn't be in the database. + + Returns true if all states and events are purged. + """ _LOGGER.debug("Cleanup filtered data") database_engine = instance.database_engine assert database_engine is not None @@ -639,7 +642,7 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: # Check if excluded entity_ids are in database entity_filter = instance.entity_filter - has_more_states_to_purge = False + has_more_to_purge = False excluded_metadata_ids: list[str] = [ metadata_id for (metadata_id, entity_id) in session.query( @@ -648,12 +651,11 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: if entity_filter and not entity_filter(entity_id) ] if excluded_metadata_ids: - has_more_states_to_purge = _purge_filtered_states( + has_more_to_purge |= not _purge_filtered_states( instance, session, excluded_metadata_ids, database_engine, now_timestamp ) # Check if excluded event_types are in database - has_more_events_to_purge = False if ( event_type_to_event_type_ids := instance.event_type_manager.get_many( instance.exclude_event_types, session @@ -665,12 +667,12 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: if event_type_id is not None ] ): - has_more_events_to_purge = _purge_filtered_events( + has_more_to_purge |= not _purge_filtered_events( instance, session, excluded_event_type_ids, now_timestamp ) # Purge has completed if there are not more state or events to purge - return not (has_more_states_to_purge or has_more_events_to_purge) + return not has_more_to_purge def _purge_filtered_states( diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 245acf4603de..e0b3f7ca8a86 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -964,6 +964,171 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 0 +@pytest.mark.parametrize( + "recorder_config", [{"exclude": {"entities": ["sensor.excluded"]}}] +) +async def test_purge_filtered_states_multiple_rounds( + hass: HomeAssistant, + recorder_mock: Recorder, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test filtered states are purged when there are multiple rounds to purge.""" + assert recorder_mock.entity_filter("sensor.excluded") is False + + def _add_db_entries(hass: HomeAssistant) -> None: + with session_scope(hass=hass) as session: + # Add states and state_changed events that should be purged + for days in range(1, 4): + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(1000, 1020): + _add_state_with_state_attributes( + session, + "sensor.excluded", + "purgeme", + timestamp, + event_id * days, + ) + # Add state **without** state_changed event that should be purged + timestamp = dt_util.utcnow() - timedelta(days=1) + session.add( + States( + entity_id="sensor.excluded", + state="purgeme", + attributes="{}", + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), + ) + ) + # Add states and state_changed events that should be keeped + timestamp = dt_util.utcnow() - timedelta(days=2) + for event_id in range(200, 210): + _add_state_with_state_attributes( + session, + "sensor.keep", + "keep", + timestamp, + event_id, + ) + # Add states with linked old_state_ids that need to be handled + timestamp = dt_util.utcnow() - timedelta(days=0) + state_attrs = StateAttributes( + hash=0, + shared_attrs=json.dumps( + {"sensor.linked_old_state_id": "sensor.linked_old_state_id"} + ), + ) + state_1 = States( + entity_id="sensor.linked_old_state_id", + state="keep", + attributes="{}", + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), + old_state_id=1, + state_attributes=state_attrs, + ) + timestamp = dt_util.utcnow() - timedelta(days=4) + state_2 = States( + entity_id="sensor.linked_old_state_id", + state="keep", + attributes="{}", + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), + old_state_id=2, + state_attributes=state_attrs, + ) + state_3 = States( + entity_id="sensor.linked_old_state_id", + state="keep", + attributes="{}", + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), + old_state_id=62, # keep + state_attributes=state_attrs, + ) + session.add_all((state_attrs, state_1, state_2, state_3)) + # Add event that should be keeped + session.add( + Events( + event_id=100, + event_type="EVENT_KEEP", + event_data="{}", + origin="LOCAL", + time_fired_ts=dt_util.utc_to_timestamp(timestamp), + ) + ) + convert_pending_states_to_meta(recorder_mock, session) + convert_pending_events_to_event_types(recorder_mock, session) + + service_data = {"keep_days": 10, "apply_filter": True} + _add_db_entries(hass) + + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 74 + events_keep = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(("EVENT_KEEP",))) + ) + assert events_keep.count() == 1 + + await hass.services.async_call( + RECORDER_DOMAIN, SERVICE_PURGE, service_data, blocking=True + ) + + for _ in range(2): + # Make sure the second round of purging runs + await async_recorder_block_till_done(hass) + await async_wait_purge_done(hass) + + assert "Cleanup filtered data hasn't fully completed yet" in caplog.text + caplog.clear() + + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 13 + events_keep = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(("EVENT_KEEP",))) + ) + assert events_keep.count() == 1 + + states_sensor_excluded = ( + session.query(States) + .outerjoin(StatesMeta, States.metadata_id == StatesMeta.metadata_id) + .filter(StatesMeta.entity_id == "sensor.excluded") + ) + assert states_sensor_excluded.count() == 0 + query = session.query(States) + + assert query.filter(States.state_id == 72).first().old_state_id is None + assert query.filter(States.state_id == 72).first().attributes_id == 71 + assert query.filter(States.state_id == 73).first().old_state_id is None + assert query.filter(States.state_id == 73).first().attributes_id == 71 + + final_keep_state = session.query(States).filter(States.state_id == 74).first() + assert final_keep_state.old_state_id == 62 # should have been kept + assert final_keep_state.attributes_id == 71 + + assert session.query(StateAttributes).count() == 11 + + # Do it again to make sure nothing changes + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) + await async_recorder_block_till_done(hass) + await async_wait_purge_done(hass) + + with session_scope(hass=hass) as session: + final_keep_state = session.query(States).filter(States.state_id == 74).first() + assert final_keep_state.old_state_id == 62 # should have been kept + assert final_keep_state.attributes_id == 71 + + assert session.query(StateAttributes).count() == 11 + + for _ in range(2): + # Make sure the second round of purging runs + await async_recorder_block_till_done(hass) + await async_wait_purge_done(hass) + + assert "Cleanup filtered data hasn't fully completed yet" not in caplog.text + + @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) @pytest.mark.parametrize( "recorder_config", [{"exclude": {"entities": ["sensor.excluded"]}}] From 5deba1766e402ef316e4d98dd702b293a7d3423a Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:49:30 +0100 Subject: [PATCH 096/123] Fix and bump apsystems-ez1 to 2.4.0 (#130740) --- homeassistant/components/apsystems/__init__.py | 1 + homeassistant/components/apsystems/manifest.json | 2 +- homeassistant/components/apsystems/switch.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 372ce52e0498..c437f5584db6 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -38,6 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> ip_address=entry.data[CONF_IP_ADDRESS], port=entry.data.get(CONF_PORT, DEFAULT_PORT), timeout=8, + enable_debounce=True, ) coordinator = ApSystemsDataCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index 9376d21ba289..a58530b05e23 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/apsystems", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["apsystems-ez1==2.2.1"] + "requirements": ["apsystems-ez1==2.4.0"] } diff --git a/homeassistant/components/apsystems/switch.py b/homeassistant/components/apsystems/switch.py index 93a21ec9f05a..739148454451 100644 --- a/homeassistant/components/apsystems/switch.py +++ b/homeassistant/components/apsystems/switch.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from aiohttp.client_exceptions import ClientConnectionError +from APsystemsEZ1 import InverterReturnedError from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import HomeAssistant @@ -40,7 +41,7 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity): """Update switch status and availability.""" try: status = await self._api.get_device_power_status() - except (TimeoutError, ClientConnectionError): + except (TimeoutError, ClientConnectionError, InverterReturnedError): self._attr_available = False else: self._attr_available = True diff --git a/requirements_all.txt b/requirements_all.txt index 9164882565f1..6fb869e68a7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -474,7 +474,7 @@ apprise==1.9.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==2.2.1 +apsystems-ez1==2.4.0 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2540db8e09cb..470aed8a4bfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ apprise==1.9.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==2.2.1 +apsystems-ez1==2.4.0 # homeassistant.components.aranet aranet4==2.4.0 From a48f88033d853a418149a569bf7513775ab946f7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 16 Nov 2024 17:40:01 +0100 Subject: [PATCH 097/123] Fix file uploads in MQTT config flow not processed in executor (#130746) Process file uploads in MQTT config flow in executor --- homeassistant/components/mqtt/config_flow.py | 25 +++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index e94f734069ac..3ed88d0d8233 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -33,7 +33,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import config_validation as cv from homeassistant.helpers.hassio import is_hassio @@ -737,6 +737,16 @@ class MQTTOptionsFlowHandler(OptionsFlow): ) +async def _get_uploaded_file(hass: HomeAssistant, id: str) -> str: + """Get file content from uploaded file.""" + + def _proces_uploaded_file() -> str: + with process_uploaded_file(hass, id) as file_path: + return file_path.read_text(encoding=DEFAULT_ENCODING) + + return await hass.async_add_executor_job(_proces_uploaded_file) + + async def async_get_broker_settings( flow: ConfigFlow | OptionsFlow, fields: OrderedDict[Any, Any], @@ -795,8 +805,7 @@ async def async_get_broker_settings( return False certificate_id: str | None = user_input.get(CONF_CERTIFICATE) if certificate_id: - with process_uploaded_file(hass, certificate_id) as certificate_file: - certificate = certificate_file.read_text(encoding=DEFAULT_ENCODING) + certificate = await _get_uploaded_file(hass, certificate_id) # Return to form for file upload CA cert or client cert and key if ( @@ -812,15 +821,9 @@ async def async_get_broker_settings( return False if client_certificate_id: - with process_uploaded_file( - hass, client_certificate_id - ) as client_certificate_file: - client_certificate = client_certificate_file.read_text( - encoding=DEFAULT_ENCODING - ) + client_certificate = await _get_uploaded_file(hass, client_certificate_id) if client_key_id: - with process_uploaded_file(hass, client_key_id) as key_file: - client_key = key_file.read_text(encoding=DEFAULT_ENCODING) + client_key = await _get_uploaded_file(hass, client_key_id) certificate_data: dict[str, Any] = {} if certificate: From 876112ff545d7d25008bb589970cccd18353e8eb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Nov 2024 15:06:41 +0100 Subject: [PATCH 098/123] Update twentemilieu to 2.1.0 (#130752) --- homeassistant/components/twentemilieu/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index aef70aa6a101..8ba4f3b760e7 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["twentemilieu"], "quality_scale": "platinum", - "requirements": ["twentemilieu==2.0.1"] + "requirements": ["twentemilieu==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6fb869e68a7d..2b8693020ed6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2870,7 +2870,7 @@ ttn_client==1.2.0 tuya-device-sharing-sdk==0.1.9 # homeassistant.components.twentemilieu -twentemilieu==2.0.1 +twentemilieu==2.1.0 # homeassistant.components.twilio twilio==6.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 470aed8a4bfd..b9052dbed6db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2283,7 +2283,7 @@ ttn_client==1.2.0 tuya-device-sharing-sdk==0.1.9 # homeassistant.components.twentemilieu -twentemilieu==2.0.1 +twentemilieu==2.1.0 # homeassistant.components.twilio twilio==6.32.0 From a588ced2e3e580c8997e4794776cbc8c0e4e8fd7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:58:25 +0100 Subject: [PATCH 099/123] Fix unexpected stop of media playback via ffmpeg proxy for ESPhome devices (#130788) disable writing progress stats to stderr in ffmpeg command --- homeassistant/components/esphome/ffmpeg_proxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/esphome/ffmpeg_proxy.py b/homeassistant/components/esphome/ffmpeg_proxy.py index cefe87f49ba5..2dacae52f75a 100644 --- a/homeassistant/components/esphome/ffmpeg_proxy.py +++ b/homeassistant/components/esphome/ffmpeg_proxy.py @@ -179,6 +179,9 @@ class FFmpegConvertResponse(web.StreamResponse): # Remove metadata and cover art command_args.extend(["-map_metadata", "-1", "-vn"]) + # disable progress stats on stderr + command_args.append("-nostats") + # Output to stdout command_args.append("pipe:") From 5b1aca53ac37bfeed42e256a1b5650da27eb18d0 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:11:51 +0100 Subject: [PATCH 100/123] Bump homematicip to 1.1.3 (#130824) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b3e7eb9a72ad..97af964ffc78 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["homematicip"], "quality_scale": "silver", - "requirements": ["homematicip==1.1.2"] + "requirements": ["homematicip==1.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b8693020ed6..24d60b016444 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ home-assistant-intents==2024.11.6 homeconnect==0.8.0 # homeassistant.components.homematicip_cloud -homematicip==1.1.2 +homematicip==1.1.3 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9052dbed6db..d1bf3b833ec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -959,7 +959,7 @@ home-assistant-intents==2024.11.6 homeconnect==0.8.0 # homeassistant.components.homematicip_cloud -homematicip==1.1.2 +homematicip==1.1.3 # homeassistant.components.remember_the_milk httplib2==0.20.4 From a024acf09618f9c0da880277f480c1b66ee5a779 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 18 Nov 2024 04:56:47 -0500 Subject: [PATCH 101/123] UPB integration: Change unique ID from int to string. (#130832) --- homeassistant/components/upb/__init__.py | 21 +++++++++++++++++ homeassistant/components/upb/config_flow.py | 3 ++- tests/components/upb/test_init.py | 25 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/components/upb/test_init.py diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index ca4375d12328..c9f3a2df1050 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -1,5 +1,7 @@ """Support the UPB PIM.""" +import logging + import upb_lib from homeassistant.config_entries import ConfigEntry @@ -14,6 +16,7 @@ from .const import ( EVENT_UPB_SCENE_CHANGED, ) +_LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.LIGHT, Platform.SCENE] @@ -63,3 +66,21 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> upb.disconnect() hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate entry.""" + + _LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + # 1 -> 2: Unique ID from integer to string + if entry.minor_version == 1: + minor_version = 2 + hass.config_entries.async_update_entry( + entry, unique_id=str(entry.unique_id), minor_version=minor_version + ) + + _LOGGER.debug("Migration successful") + + return True diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index d9f111049fd9..788a0336d730 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -78,6 +78,7 @@ class UPBConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for UPB PIM.""" VERSION = 1 + MINOR_VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -98,7 +99,7 @@ class UPBConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if "base" not in errors: - await self.async_set_unique_id(network_id) + await self.async_set_unique_id(str(network_id)) self._abort_if_unique_id_configured() return self.async_create_entry( diff --git a/tests/components/upb/test_init.py b/tests/components/upb/test_init.py new file mode 100644 index 000000000000..a7621ce65fe5 --- /dev/null +++ b/tests/components/upb/test_init.py @@ -0,0 +1,25 @@ +"""The init tests for the UPB platform.""" + +from unittest.mock import patch + +from homeassistant.components.upb.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_migrate_entry_minor_version_1_2(hass: HomeAssistant) -> None: + """Test migrating a 1.1 config entry to 1.2.""" + with patch("homeassistant.components.upb.async_setup_entry", return_value=True): + entry = MockConfigEntry( + domain=DOMAIN, + data={"protocol": "TCP", "address": "1.2.3.4", "file_path": "upb.upe"}, + version=1, + minor_version=1, + unique_id=123456, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.unique_id == "123456" From 04bc04117419b9dd2b2d27c94870db157b163b70 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Nov 2024 18:59:17 +0100 Subject: [PATCH 102/123] Reolink fix dev/entity id migration (#130836) --- homeassistant/components/reolink/__init__.py | 30 ++++- tests/components/reolink/test_init.py | 110 +++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 7a36991201a8..ae0badb3d84f 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -326,7 +326,19 @@ def migrate_entity_ids( else: new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" new_identifiers = {(DOMAIN, new_device_id)} - device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + existing_device = device_reg.async_get_device(identifiers=new_identifiers) + if existing_device is None: + device_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + else: + _LOGGER.warning( + "Reolink device with uid %s already exists, " + "removing device with uid %s", + new_device_id, + device_uid, + ) + device_reg.async_remove_device(device.id) entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) @@ -352,4 +364,18 @@ def migrate_entity_ids( id_parts = entity.unique_id.split("_", 2) if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch): new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}" - entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id) + existing_entity = entity_reg.async_get_entity_id( + entity.domain, entity.platform, new_id + ) + if existing_entity is None: + entity_reg.async_update_entity( + entity.entity_id, new_unique_id=new_id + ) + else: + _LOGGER.warning( + "Reolink entity with unique_id %s already exists, " + "removing device with unique_id %s", + new_id, + entity.unique_id, + ) + entity_reg.async_remove(entity.entity_id) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 67ac2db82627..f851e13c91dd 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -469,6 +469,116 @@ async def test_migrate_entity_ids( assert device_registry.async_get_device(identifiers={(DOMAIN, new_dev_id)}) +async def test_migrate_with_already_existing_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device ids that need to be migrated while the new ids already exist.""" + original_dev_id = f"{TEST_MAC}_ch0" + new_dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + + def mock_supported(ch, capability): + if capability == "UID" and ch is None: + return True + if capability == "UID": + return True + return True + + reolink_connect.channels = [0] + reolink_connect.supported = mock_supported + + device_registry.async_get_or_create( + identifiers={(DOMAIN, new_dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + device_registry.async_get_or_create( + identifiers={(DOMAIN, original_dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + assert device_registry.async_get_device(identifiers={(DOMAIN, original_dev_id)}) + assert device_registry.async_get_device(identifiers={(DOMAIN, new_dev_id)}) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, original_dev_id)}) + is None + ) + assert device_registry.async_get_device(identifiers={(DOMAIN, new_dev_id)}) + + +async def test_migrate_with_already_existing_entity( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test entity ids that need to be migrated while the new ids already exist.""" + original_id = f"{TEST_UID}_0_record_audio" + new_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + + def mock_supported(ch, capability): + if capability == "UID" and ch is None: + return True + if capability == "UID": + return True + return True + + reolink_connect.channels = [0] + reolink_connect.supported = mock_supported + + dev_entry = device_registry.async_get_or_create( + identifiers={(DOMAIN, dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=new_id, + config_entry=config_entry, + suggested_object_id=new_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=original_id, + config_entry=config_entry, + suggested_object_id=original_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) + assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) is None + assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From 6944ba0333e29b7e34d763c02a217f14d2bce2f8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 17 Nov 2024 23:28:54 +0100 Subject: [PATCH 103/123] Use default device sensors also for AirQ devices in Sensibo (#130841) --- homeassistant/components/sensibo/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index a6a70ea6c493..b395f8eb1eeb 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -178,6 +178,7 @@ AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.co2, extra_fn=None, ), + *DEVICE_SENSOR_TYPES, ) ELEMENT_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( From 6853234f9da4fc8575796211ada88381ae967b1f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:19:40 +0100 Subject: [PATCH 104/123] Pass config_entry explicitly in rachio (#130865) --- homeassistant/components/rachio/coordinator.py | 5 +++++ homeassistant/components/rachio/device.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rachio/coordinator.py b/homeassistant/components/rachio/coordinator.py index 25c40bd6656d..62d42f2afdae 100644 --- a/homeassistant/components/rachio/coordinator.py +++ b/homeassistant/components/rachio/coordinator.py @@ -8,6 +8,7 @@ from typing import Any from rachiopy import Rachio from requests.exceptions import Timeout +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -38,6 +39,7 @@ class RachioUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self, hass: HomeAssistant, rachio: Rachio, + config_entry: ConfigEntry, base_station, base_count: int, ) -> None: @@ -48,6 +50,7 @@ class RachioUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"{DOMAIN} update coordinator", # To avoid exceeding the rate limit, increase polling interval for # each additional base station on the account @@ -76,6 +79,7 @@ class RachioScheduleUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]] self, hass: HomeAssistant, rachio: Rachio, + config_entry: ConfigEntry, base_station, ) -> None: """Initialize a Rachio schedule coordinator.""" @@ -85,6 +89,7 @@ class RachioScheduleUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]] super().__init__( hass, _LOGGER, + config_entry=config_entry, name=f"{DOMAIN} schedule update coordinator", update_interval=timedelta(minutes=30), ) diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index f06910cd5051..179e5f5ec0d8 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -189,8 +189,10 @@ class RachioPerson: RachioBaseStation( rachio, base, - RachioUpdateCoordinator(hass, rachio, base, base_count), - RachioScheduleUpdateCoordinator(hass, rachio, base), + RachioUpdateCoordinator( + hass, rachio, self.config_entry, base, base_count + ), + RachioScheduleUpdateCoordinator(hass, rachio, self.config_entry, base), ) for base in base_stations ) From 9447180c047a43e7bbd560f1ef4335a6abcd6f9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Nov 2024 12:56:33 -0500 Subject: [PATCH 105/123] Bump bluetooth-adapters to 0.20.2 (#130877) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index fe16bd73a9e7..e25c077b57fa 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.22.3", "bleak-retry-connector==3.6.0", - "bluetooth-adapters==0.20.0", + "bluetooth-adapters==0.20.2", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.20.0", "dbus-fast==2.24.3", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index caa30fa24d16..1f52c4c8b18b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ awesomeversion==24.6.0 bcrypt==4.2.0 bleak-retry-connector==3.6.0 bleak==0.22.3 -bluetooth-adapters==0.20.0 +bluetooth-adapters==0.20.2 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.20.0 cached-ipaddress==0.8.0 diff --git a/requirements_all.txt b/requirements_all.txt index 24d60b016444..6d8c6221a0d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -607,7 +607,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.20.0 +bluetooth-adapters==0.20.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1bf3b833ec8..97d71a36a026 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -531,7 +531,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.20.0 +bluetooth-adapters==0.20.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From aaa36adbcc90713f53cbb3096c9c26672f57fc6d Mon Sep 17 00:00:00 2001 From: Charles Yuan <70110720+CharlesYuan02@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:23:32 -0500 Subject: [PATCH 106/123] Fixed Small Inaccuracy in Description String for myUplink (#130900) --- homeassistant/components/myuplink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/myuplink/strings.json b/homeassistant/components/myuplink/strings.json index 9ec5c355d782..997c6fe54b6a 100644 --- a/homeassistant/components/myuplink/strings.json +++ b/homeassistant/components/myuplink/strings.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Follow the [instructions]({more_info_url}) to give Home Assistant access to your myUplink account. You also need to create application credentials linked to your account:\n1. Go to [Applications at myUplink developer site]({create_creds_url}) and get credentials from an existing application or select **Create New Application**.\n1. Set appropriate Application name and Description\n2. Enter `{callback_url}` as Callback Url" + "description": "Follow the [instructions]({more_info_url}) to give Home Assistant access to your myUplink account. You also need to create application credentials linked to your account:\n1. Go to [Applications at myUplink developer site]({create_creds_url}) and get credentials from an existing application or select **Create New Application**.\n1. Set appropriate Application name and Description\n1. Enter `{callback_url}` as Callback URL" }, "config": { "step": { From 477141c22a3f9d4c6d53374f3c9081182e45c73a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Wed, 20 Nov 2024 18:21:16 +0100 Subject: [PATCH 107/123] Unscape HTML Entities from RSS feeds (#130915) * Unscape HTML Entities from RSS feeds * Improve tests --- .../components/feedreader/config_flow.py | 3 +- .../components/feedreader/coordinator.py | 4 ++- homeassistant/components/feedreader/event.py | 12 +++++-- tests/components/feedreader/conftest.py | 12 +++++++ .../feedreader/fixtures/feedreader10.xml | 19 ++++++++++ .../feedreader/fixtures/feedreader9.xml | 21 +++++++++++ .../feedreader/snapshots/test_event.ambr | 27 ++++++++++++++ .../components/feedreader/test_config_flow.py | 35 +++++++++++++++++++ tests/components/feedreader/test_event.py | 31 ++++++++++++++++ tests/components/feedreader/test_init.py | 21 +++++++++++ 10 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 tests/components/feedreader/fixtures/feedreader10.xml create mode 100644 tests/components/feedreader/fixtures/feedreader9.xml create mode 100644 tests/components/feedreader/snapshots/test_event.ambr diff --git a/homeassistant/components/feedreader/config_flow.py b/homeassistant/components/feedreader/config_flow.py index 2a73e24a3e52..e05cf9a63e57 100644 --- a/homeassistant/components/feedreader/config_flow.py +++ b/homeassistant/components/feedreader/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import html import logging from typing import Any import urllib.error @@ -106,7 +107,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN): return self.abort_on_import_error(user_input[CONF_URL], "url_error") return self.show_user_form(user_input, {"base": "url_error"}) - feed_title = feed["feed"]["title"] + feed_title = html.unescape(feed["feed"]["title"]) return self.async_create_entry( title=feed_title, diff --git a/homeassistant/components/feedreader/coordinator.py b/homeassistant/components/feedreader/coordinator.py index 6608c4312fe1..f45b303946aa 100644 --- a/homeassistant/components/feedreader/coordinator.py +++ b/homeassistant/components/feedreader/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations from calendar import timegm from datetime import datetime +import html from logging import getLogger from time import gmtime, struct_time from typing import TYPE_CHECKING @@ -102,7 +103,8 @@ class FeedReaderCoordinator( """Set up the feed manager.""" feed = await self._async_fetch_feed() self.logger.debug("Feed data fetched from %s : %s", self.url, feed["feed"]) - self.feed_author = feed["feed"].get("author") + if feed_author := feed["feed"].get("author"): + self.feed_author = html.unescape(feed_author) self.feed_version = feedparser.api.SUPPORTED_VERSIONS.get(feed["version"]) self._feed = feed diff --git a/homeassistant/components/feedreader/event.py b/homeassistant/components/feedreader/event.py index 4b3fb2e25249..ad6aed0fc765 100644 --- a/homeassistant/components/feedreader/event.py +++ b/homeassistant/components/feedreader/event.py @@ -2,6 +2,7 @@ from __future__ import annotations +import html import logging from feedparser import FeedParserDict @@ -76,15 +77,22 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity): # so we always take the first entry in list, since we only care about the latest entry feed_data: FeedParserDict = data[0] + if description := feed_data.get("description"): + description = html.unescape(description) + + if title := feed_data.get("title"): + title = html.unescape(title) + if content := feed_data.get("content"): if isinstance(content, list) and isinstance(content[0], dict): content = content[0].get("value") + content = html.unescape(content) self._trigger_event( EVENT_FEEDREADER, { - ATTR_DESCRIPTION: feed_data.get("description"), - ATTR_TITLE: feed_data.get("title"), + ATTR_DESCRIPTION: description, + ATTR_TITLE: title, ATTR_LINK: feed_data.get("link"), ATTR_CONTENT: content, }, diff --git a/tests/components/feedreader/conftest.py b/tests/components/feedreader/conftest.py index 8eeb89e00cda..1e7d50c3835c 100644 --- a/tests/components/feedreader/conftest.py +++ b/tests/components/feedreader/conftest.py @@ -64,6 +64,18 @@ def fixture_feed_only_summary(hass: HomeAssistant) -> bytes: return load_fixture_bytes("feedreader8.xml") +@pytest.fixture(name="feed_htmlentities") +def fixture_feed_htmlentities(hass: HomeAssistant) -> bytes: + """Load test feed data with HTML Entities.""" + return load_fixture_bytes("feedreader9.xml") + + +@pytest.fixture(name="feed_atom_htmlentities") +def fixture_feed_atom_htmlentities(hass: HomeAssistant) -> bytes: + """Load test ATOM feed data with HTML Entities.""" + return load_fixture_bytes("feedreader10.xml") + + @pytest.fixture(name="events") async def fixture_events(hass: HomeAssistant) -> list[Event]: """Fixture that catches alexa events.""" diff --git a/tests/components/feedreader/fixtures/feedreader10.xml b/tests/components/feedreader/fixtures/feedreader10.xml new file mode 100644 index 000000000000..17ec8069ae1a --- /dev/null +++ b/tests/components/feedreader/fixtures/feedreader10.xml @@ -0,0 +1,19 @@ + + + <![CDATA[ATOM RSS en español]]> + + 2024-11-18T14:00:00Z + + + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + + <![CDATA[Título]]> + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2024-11-18T14:00:00Z + + + + diff --git a/tests/components/feedreader/fixtures/feedreader9.xml b/tests/components/feedreader/fixtures/feedreader9.xml new file mode 100644 index 000000000000..580a42cbd3f6 --- /dev/null +++ b/tests/components/feedreader/fixtures/feedreader9.xml @@ -0,0 +1,21 @@ + + + + <![CDATA[RSS en español]]> + + http://www.example.com/main.html + Mon, 18 Nov 2024 15:00:00 +1000 + Mon, 18 Nov 2024 15:00:00 +1000 + 1800 + + + <![CDATA[Título 1]]> + + http://www.example.com/link/1 + GUID 1 + Mon, 18 Nov 2024 15:00:00 +1000 + + + + + diff --git a/tests/components/feedreader/snapshots/test_event.ambr b/tests/components/feedreader/snapshots/test_event.ambr new file mode 100644 index 000000000000..9cce035ea87d --- /dev/null +++ b/tests/components/feedreader/snapshots/test_event.ambr @@ -0,0 +1,27 @@ +# serializer version: 1 +# name: test_event_htmlentities[feed_atom_htmlentities] + ReadOnlyDict({ + 'content': 'Contenido en español', + 'description': 'Resumen en español', + 'event_type': 'feedreader', + 'event_types': list([ + 'feedreader', + ]), + 'friendly_name': 'Mock Title', + 'link': 'http://example.org/2003/12/13/atom03', + 'title': 'Título', + }) +# --- +# name: test_event_htmlentities[feed_htmlentities] + ReadOnlyDict({ + 'content': 'Contenido 1 en español', + 'description': 'Descripción 1', + 'event_type': 'feedreader', + 'event_types': list([ + 'feedreader', + ]), + 'friendly_name': 'Mock Title', + 'link': 'http://www.example.com/link/1', + 'title': 'Título 1', + }) +# --- diff --git a/tests/components/feedreader/test_config_flow.py b/tests/components/feedreader/test_config_flow.py index 2a434306c0f6..e801227293c3 100644 --- a/tests/components/feedreader/test_config_flow.py +++ b/tests/components/feedreader/test_config_flow.py @@ -246,3 +246,38 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["data"] == { CONF_MAX_ENTRIES: 10, } + + +@pytest.mark.parametrize( + ("fixture_name", "expected_title"), + [ + ("feed_htmlentities", "RSS en español"), + ("feed_atom_htmlentities", "ATOM RSS en español"), + ], +) +async def test_feed_htmlentities( + hass: HomeAssistant, + feedparser, + setup_entry, + fixture_name, + expected_title, + request: pytest.FixtureRequest, +) -> None: + """Test starting a flow by user from a feed with HTML Entities in the title.""" + with patch( + "homeassistant.components.feedreader.config_flow.feedparser.http.get", + side_effect=[request.getfixturevalue(fixture_name)], + ): + # init user flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # success + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: URL} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == expected_title diff --git a/tests/components/feedreader/test_event.py b/tests/components/feedreader/test_event.py index 491c7e38d022..32f8ecb8080f 100644 --- a/tests/components/feedreader/test_event.py +++ b/tests/components/feedreader/test_event.py @@ -3,6 +3,9 @@ from datetime import timedelta from unittest.mock import patch +import pytest +from syrupy.assertion import SnapshotAssertion + from homeassistant.components.feedreader.event import ( ATTR_CONTENT, ATTR_DESCRIPTION, @@ -59,3 +62,31 @@ async def test_event_entity( assert state.attributes[ATTR_LINK] == "http://www.example.com/link/1" assert state.attributes[ATTR_CONTENT] == "This is a summary" assert state.attributes[ATTR_DESCRIPTION] == "Description 1" + + +@pytest.mark.parametrize( + ("fixture_name"), + [ + ("feed_htmlentities"), + ("feed_atom_htmlentities"), + ], +) +async def test_event_htmlentities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + fixture_name, + request: pytest.FixtureRequest, +) -> None: + """Test feed event entity with HTML Entities.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get", + side_effect=[request.getfixturevalue(fixture_name)], + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("event.mock_title") + assert state + assert state.attributes == snapshot diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index d7700d79e3b2..bc7a66dc86ea 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -12,6 +12,7 @@ import pytest from homeassistant.components.feedreader.const import DOMAIN from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import device_registry as dr import homeassistant.util.dt as dt_util from . import async_setup_config_entry, create_mock_entry @@ -357,3 +358,23 @@ async def test_feed_errors( freezer.tick(timedelta(hours=1, seconds=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_feed_atom_htmlentities( + hass: HomeAssistant, feed_atom_htmlentities, device_registry: dr.DeviceRegistry +) -> None: + """Test ATOM feed author with HTML Entities.""" + + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get", + side_effect=[feed_atom_htmlentities], + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, entry.entry_id)} + ) + assert device_entry.manufacturer == "Juan Pérez" From fc607ea7e5a9d5d00a48200e75afb825b5860835 Mon Sep 17 00:00:00 2001 From: ElmaxSrl <93348307+ElmaxSrl@users.noreply.github.com> Date: Fri, 22 Nov 2024 08:52:14 +0100 Subject: [PATCH 108/123] Update elmax_api to v0.0.6.1 (#130917) Co-authored-by: Alberto Geniola --- homeassistant/components/elmax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elmax/manifest.json b/homeassistant/components/elmax/manifest.json index c57b707906b3..efa97a9f6b95 100644 --- a/homeassistant/components/elmax/manifest.json +++ b/homeassistant/components/elmax/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/elmax", "iot_class": "cloud_polling", "loggers": ["elmax_api"], - "requirements": ["elmax-api==0.0.5"], + "requirements": ["elmax-api==0.0.6.1"], "zeroconf": [ { "type": "_elmax-ssl._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 6d8c6221a0d9..6f4b22cb0c94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -814,7 +814,7 @@ eliqonline==1.2.2 elkm1-lib==2.2.10 # homeassistant.components.elmax -elmax-api==0.0.5 +elmax-api==0.0.6.1 # homeassistant.components.elvia elvia==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97d71a36a026..c79175b7b241 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -689,7 +689,7 @@ elgato==5.1.2 elkm1-lib==2.2.10 # homeassistant.components.elmax -elmax-api==0.0.5 +elmax-api==0.0.6.1 # homeassistant.components.elvia elvia==0.1.0 From 4753510ace2cc615ce2429a0520434517ecb916e Mon Sep 17 00:00:00 2001 From: Renat Sibgatulin Date: Tue, 19 Nov 2024 20:13:33 +0100 Subject: [PATCH 109/123] Bump aioairq to 0.4.3 (#130963) --- homeassistant/components/airq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airq/manifest.json b/homeassistant/components/airq/manifest.json index 2b23928aba87..1ae7da148753 100644 --- a/homeassistant/components/airq/manifest.json +++ b/homeassistant/components/airq/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioairq"], - "requirements": ["aioairq==0.3.2"] + "requirements": ["aioairq==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6f4b22cb0c94..12fe14e93ca9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aio-geojson-usgs-earthquakes==0.3 aio-georss-gdacs==0.10 # homeassistant.components.airq -aioairq==0.3.2 +aioairq==0.4.3 # homeassistant.components.airzone_cloud aioairzone-cloud==0.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c79175b7b241..fd33bbb1b14e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-geojson-usgs-earthquakes==0.3 aio-georss-gdacs==0.10 # homeassistant.components.airq -aioairq==0.3.2 +aioairq==0.4.3 # homeassistant.components.airzone_cloud aioairzone-cloud==0.6.10 From a75ce850b88816b373ffdb6a56e90032efba69ee Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 20 Nov 2024 03:57:57 +0100 Subject: [PATCH 110/123] Strip whitespaces from host in ping config flow (#130970) --- homeassistant/components/ping/config_flow.py | 9 ++++- tests/components/ping/test_config_flow.py | 36 ++++++++++++-------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ping/config_flow.py b/homeassistant/components/ping/config_flow.py index 9470b2134d44..505e0a370a03 100644 --- a/homeassistant/components/ping/config_flow.py +++ b/homeassistant/components/ping/config_flow.py @@ -27,6 +27,12 @@ from .const import CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN _LOGGER = logging.getLogger(__name__) +def _clean_user_input(user_input: dict[str, Any]) -> dict[str, Any]: + """Clean up the user input.""" + user_input[CONF_HOST] = user_input[CONF_HOST].strip() + return user_input + + class PingConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ping.""" @@ -46,6 +52,7 @@ class PingConfigFlow(ConfigFlow, domain=DOMAIN): ), ) + user_input = _clean_user_input(user_input) if not is_ip_address(user_input[CONF_HOST]): self.async_abort(reason="invalid_ip_address") @@ -81,7 +88,7 @@ class OptionsFlowHandler(OptionsFlow): ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(title="", data=_clean_user_input(user_input)) return self.async_show_form( step_id="init", diff --git a/tests/components/ping/test_config_flow.py b/tests/components/ping/test_config_flow.py index 8204a000f29b..bc13030647eb 100644 --- a/tests/components/ping/test_config_flow.py +++ b/tests/components/ping/test_config_flow.py @@ -13,11 +13,15 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - ("host", "expected_title"), - [("192.618.178.1", "192.618.178.1")], + ("host", "expected"), + [ + ("192.618.178.1", "192.618.178.1"), + (" 192.618.178.1 ", "192.618.178.1"), + (" demo.host ", "demo.host"), + ], ) @pytest.mark.usefixtures("patch_setup") -async def test_form(hass: HomeAssistant, host, expected_title) -> None: +async def test_form(hass: HomeAssistant, host, expected) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -35,21 +39,25 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None: await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == expected_title + assert result["title"] == expected assert result["data"] == {} assert result["options"] == { "count": 5, - "host": host, + "host": expected, "consider_home": 180, } @pytest.mark.parametrize( - ("host", "count", "expected_title"), - [("192.618.178.1", 10, "192.618.178.1")], + ("host", "expected_host"), + [ + ("192.618.178.1", "192.618.178.1"), + (" 192.618.178.1 ", "192.618.178.1"), + (" demo.host ", "demo.host"), + ], ) @pytest.mark.usefixtures("patch_setup") -async def test_options(hass: HomeAssistant, host, count, expected_title) -> None: +async def test_options(hass: HomeAssistant, host: str, expected_host: str) -> None: """Test options flow.""" config_entry = MockConfigEntry( @@ -57,8 +65,8 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None source=config_entries.SOURCE_USER, data={}, domain=DOMAIN, - options={"count": count, "host": host, "consider_home": 180}, - title=expected_title, + options={"count": 1, "host": "192.168.1.1", "consider_home": 180}, + title="192.168.1.1", ) config_entry.add_to_hass(hass) @@ -72,15 +80,15 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None result = await hass.config_entries.options.async_configure( result["flow_id"], { - "host": "10.10.10.1", - "count": count, + "host": host, + "count": 10, }, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { - "count": count, - "host": "10.10.10.1", + "count": 10, + "host": expected_host, "consider_home": 180, } From 24ccb9b8949349f2ee90eae59b3a2a70b0c15ee7 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 19 Nov 2024 22:31:57 +0100 Subject: [PATCH 111/123] Add more UI user-friendly description to six Supervisor actions (#130971) --- homeassistant/components/hassio/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 09ed45bd5bc2..de42a317cc7d 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -274,7 +274,7 @@ "fields": { "addon": { "name": "Add-on", - "description": "The add-on slug." + "description": "The add-on to start." } } }, @@ -284,17 +284,17 @@ "fields": { "addon": { "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", - "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + "description": "The add-on to restart." } } }, "addon_stdin": { "name": "Write data to add-on stdin.", - "description": "Writes data to add-on stdin.", + "description": "Writes data to the add-on's standard input.", "fields": { "addon": { "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", - "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + "description": "The add-on to write to." } } }, @@ -304,7 +304,7 @@ "fields": { "addon": { "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", - "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + "description": "The add-on to stop." } } }, @@ -314,7 +314,7 @@ "fields": { "addon": { "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", - "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + "description": "The add-on to update." } } }, From 335124acc693185d25c62a4be113848dc002bd8d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 19 Nov 2024 20:19:53 +0100 Subject: [PATCH 112/123] Add missing catholic category in workday (#130983) --- homeassistant/components/workday/strings.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index f3b966e28ea5..e74dc0160d9c 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -86,18 +86,19 @@ "options": { "armed_forces": "Armed forces", "bank": "Bank", + "catholic": "Catholic", + "chinese": "Chinese", + "christian": "Christian", "government": "Government", "half_day": "Half day", + "hebrew": "Hebrew", + "hindu": "Hindu", + "islamic": "Islamic", "optional": "Optional", "public": "Public", "school": "School", "unofficial": "Unofficial", - "workday": "Workday", - "chinese": "Chinese", - "christian": "Christian", - "hebrew": "Hebrew", - "hindu": "Hindu", - "islamic": "Islamic" + "workday": "Workday" } }, "days": { From 8db18181d0c858b2f4b3a5b21f1b5d6f3a23d76a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 19 Nov 2024 20:53:11 +0100 Subject: [PATCH 113/123] Bump holidays to 0.61 (#130984) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 8c64f492d42b..a3c0a4514d3d 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.60", "babel==2.15.0"] + "requirements": ["holidays==0.61", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index b02db7347296..ea08bfe17176 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.60"] + "requirements": ["holidays==0.61"] } diff --git a/requirements_all.txt b/requirements_all.txt index 12fe14e93ca9..cfb5f0637062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1121,7 +1121,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.60 +holidays==0.61 # homeassistant.components.frontend home-assistant-frontend==20241106.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd33bbb1b14e..07bf25d47f79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.60 +holidays==0.61 # homeassistant.components.frontend home-assistant-frontend==20241106.2 From 50a610914bfdff1aff2025737789b1f152e9a0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 14 Nov 2024 15:27:10 +0100 Subject: [PATCH 114/123] Bump aioairzone to 0.9.6 (#130559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone to v0.9.6 Signed-off-by: Álvaro Fernández Rojas * Remove _async_migrator_mac_empty and improve tests Signed-off-by: Álvaro Fernández Rojas * Remove WebServer empty mac fixes as requested by @epenet Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 10fb20bb2ce9..6bf374087a6c 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.9.5"] + "requirements": ["aioairzone==0.9.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index cfb5f0637062..9fbb8b202522 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioairq==0.4.3 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.5 +aioairzone==0.9.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07bf25d47f79..0e61aa3b6d86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aioairq==0.4.3 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.5 +aioairzone==0.9.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 93b4570c049a776ea60c1c502fe47f83f32c9063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 20 Nov 2024 18:41:14 +0100 Subject: [PATCH 115/123] Update aioairzone to v0.9.7 (#131033) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 6bf374087a6c..01fde7eb2fbf 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.9.6"] + "requirements": ["aioairzone==0.9.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9fbb8b202522..09c36a41b526 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioairq==0.4.3 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.6 +aioairzone==0.9.7 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e61aa3b6d86..c32ab4e40b89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aioairq==0.4.3 aioairzone-cloud==0.6.10 # homeassistant.components.airzone -aioairzone==0.9.6 +aioairzone==0.9.7 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 402c668f054b04226ed988dda81089e6ee139717 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 21 Nov 2024 01:47:26 +0100 Subject: [PATCH 116/123] Replace "service" with "action" in zha:reconfigure_device (#131111) Replace "service" with "action" in one description As services are now actions in HA this needs to be fixed. --- homeassistant/components/zha/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index d0505bf2460a..d21cd1c5042b 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -297,7 +297,7 @@ }, "reconfigure_device": { "name": "Reconfigure device", - "description": "Reconfigures a ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery-powered device, ensure it is awake and accepting commands when you use this service.", + "description": "Reconfigures a ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery-powered device, ensure it is awake and accepting commands when you use this action.", "fields": { "ieee": { "name": "[%key:component::zha::services::permit::fields::ieee::name%]", From c88ff2ca44ace41a50cc9378991e6e4c880e426d Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 21 Nov 2024 10:26:40 +0100 Subject: [PATCH 117/123] Fix typo in name of "Alarm arm home instant" action (#131151) --- homeassistant/components/elkm1/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index 6318231c281a..bf02d7272800 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -68,7 +68,7 @@ } }, "alarm_arm_home_instant": { - "name": "Alarm are home instant", + "name": "Alarm arm home instant", "description": "Arms the ElkM1 in home instant mode.", "fields": { "code": { From 75dcdfb087ed67befec6783da3a4d9d9ffb96cbc Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 21 Nov 2024 13:38:34 +0100 Subject: [PATCH 118/123] Fix cast translation string (#131156) --- homeassistant/components/cast/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index 12f2edeee9a9..9c49813bd830 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -53,7 +53,7 @@ }, "view_path": { "name": "View path", - "description": "The path of the dashboard view to show." + "description": "The URL path of the dashboard view to show." } } } From 780eaa83791e5359ffd17c61d2e0202475b1a618 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:42:08 +1300 Subject: [PATCH 119/123] Fix typo in ESPHome repair text (#131200) --- homeassistant/components/esphome/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index ec7e6f674b3e..a764e9e6fd94 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -118,7 +118,7 @@ }, "service_calls_not_allowed": { "title": "{name} is not permitted to perform Home Assistant actions", - "description": "The ESPHome device attempted to perform a Home Assistant action, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to perfom Home Assistant action, you can enable this functionality in the options flow." + "description": "The ESPHome device attempted to perform a Home Assistant action, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to perform Home Assistant action, you can enable this functionality in the options flow." } } } From 44ad8081a3133027c538e5e9d1095181a70ec97c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 21 Nov 2024 20:51:38 +0100 Subject: [PATCH 120/123] Reolink log fast poll errors once (#131203) --- homeassistant/components/reolink/host.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 336876d4c4fc..68a44bf0aae2 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -110,6 +110,7 @@ class ReolinkHost: self._cancel_onvif_check: CALLBACK_TYPE | None = None self._cancel_long_poll_check: CALLBACK_TYPE | None = None self._poll_job = HassJob(self._async_poll_all_motion, cancel_on_shutdown=True) + self._fast_poll_error: bool = False self._long_poll_task: asyncio.Task | None = None self._lost_subscription: bool = False @@ -699,14 +700,20 @@ class ReolinkHost: return try: - await self._api.get_motion_state_all_ch() + if self._api.session_active: + await self._api.get_motion_state_all_ch() except ReolinkError as err: - _LOGGER.error( - "Reolink error while polling motion state for host %s:%s: %s", - self._api.host, - self._api.port, - err, - ) + if not self._fast_poll_error: + _LOGGER.error( + "Reolink error while polling motion state for host %s:%s: %s", + self._api.host, + self._api.port, + err, + ) + self._fast_poll_error = True + else: + if self._api.session_active: + self._fast_poll_error = False finally: # schedule next poll if not self._hass.is_stopping: From 2f05240e4c6dcb78f70d5521bd51ac37e0cb9b5c Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 21 Nov 2024 21:16:37 +0100 Subject: [PATCH 121/123] Fix fibaro cover state is not always correct (#131206) --- homeassistant/components/fibaro/cover.py | 59 ++++++++---------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index c787ca702726..0898d1c93184 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -69,37 +69,29 @@ class FibaroCover(FibaroEntity, CoverEntity): # so if it is missing we have a device which supports open / close only return not self.fibaro_device.value.has_value - @property - def current_cover_position(self) -> int | None: - """Return current position of cover. 0 is closed, 100 is open.""" - return self.bound(self.level) + def update(self) -> None: + """Update the state.""" + super().update() - @property - def current_cover_tilt_position(self) -> int | None: - """Return the current tilt position for venetian blinds.""" - return self.bound(self.level2) + self._attr_current_cover_position = self.bound(self.level) + self._attr_current_cover_tilt_position = self.bound(self.level2) - @property - def is_opening(self) -> bool | None: - """Return if the cover is opening or not. + device_state = self.fibaro_device.state - Be aware that this property is only available for some modern devices. - For example the Fibaro Roller Shutter 4 reports this correctly. - """ - if self.fibaro_device.state.has_value: - return self.fibaro_device.state.str_value().lower() == "opening" - return None + # Be aware that opening and closing is only available for some modern + # devices. + # For example the Fibaro Roller Shutter 4 reports this correctly. + if device_state.has_value: + self._attr_is_opening = device_state.str_value().lower() == "opening" + self._attr_is_closing = device_state.str_value().lower() == "closing" - @property - def is_closing(self) -> bool | None: - """Return if the cover is closing or not. - - Be aware that this property is only available for some modern devices. - For example the Fibaro Roller Shutter 4 reports this correctly. - """ - if self.fibaro_device.state.has_value: - return self.fibaro_device.state.str_value().lower() == "closing" - return None + closed: bool | None = None + if self._is_open_close_only(): + if device_state.has_value and device_state.str_value().lower() != "unknown": + closed = device_state.str_value().lower() == "closed" + elif self.current_cover_position is not None: + closed = self.current_cover_position == 0 + self._attr_is_closed = closed def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" @@ -109,19 +101,6 @@ class FibaroCover(FibaroEntity, CoverEntity): """Move the cover to a specific position.""" self.set_level2(cast(int, kwargs.get(ATTR_TILT_POSITION))) - @property - def is_closed(self) -> bool | None: - """Return if the cover is closed.""" - if self._is_open_close_only(): - state = self.fibaro_device.state - if not state.has_value or state.str_value().lower() == "unknown": - return None - return state.str_value().lower() == "closed" - - if self.current_cover_position is None: - return None - return self.current_cover_position == 0 - def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self.action("open") From bfcd4194f3bd4d52f2b31f99c75fc59e8ad77417 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 22 Nov 2024 09:52:30 +0100 Subject: [PATCH 122/123] Bump reolink_aio to 0.11.2 (#131237) --- 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 7921bdb6ed58..0e2c918acc93 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.11.1"] + "requirements": ["reolink-aio==0.11.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 09c36a41b526..128f8b5e62fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2547,7 +2547,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.1 +reolink-aio==0.11.2 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c32ab4e40b89..b44385bbd0bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2038,7 +2038,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.1 +reolink-aio==0.11.2 # homeassistant.components.rflink rflink==0.0.66 From 4ef50ffd88b4a61ed99a221333505890e7162e8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 22 Nov 2024 11:05:59 +0100 Subject: [PATCH 123/123] Bump version to 2024.11.3 --- 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 a2068f71ecf6..fd2a55c0a647 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 11 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e13d9ce2739f..f25bdbefdf32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.11.2" +version = "2024.11.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"