From 724825d34c41c4ffb68e3b7764921f84e0f348c1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Apr 2025 18:59:18 +0000 Subject: [PATCH 001/110] Bump version to 2025.5.0b0 --- 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 b73aed1b8b9..03c45bc317e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 5fbf00bae8a..cf47857c2c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0.dev0" +version = "2025.5.0b0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 1cd94affd1107d867ab005329f5f7c01b08e87c0 Mon Sep 17 00:00:00 2001 From: Megamind Date: Wed, 30 Apr 2025 14:04:56 -0700 Subject: [PATCH 002/110] Bump pushover-complete to 1.2.0 (#143966) Co-authored-by: Joostlek --- homeassistant/components/pushover/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index d086321c088..e13a254c423 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/pushover", "iot_class": "cloud_push", "loggers": ["pushover_complete"], - "requirements": ["pushover_complete==1.1.1"] + "requirements": ["pushover_complete==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index aae49abd837..b32ecebfea2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1726,7 +1726,7 @@ pulsectl==23.5.2 pushbullet.py==0.11.0 # homeassistant.components.pushover -pushover_complete==1.1.1 +pushover_complete==1.2.0 # homeassistant.components.pvoutput pvo==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0788977826f..2590b02ed27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1428,7 +1428,7 @@ psutil==7.0.0 pushbullet.py==0.11.0 # homeassistant.components.pushover -pushover_complete==1.1.1 +pushover_complete==1.2.0 # homeassistant.components.pvoutput pvo==2.2.1 From a8169d205606acf6a636b96be2a3e8f6b46fd934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Wed, 30 Apr 2025 21:03:17 +0200 Subject: [PATCH 003/110] Add units of measurement for Home Connect counter entities (#143982) --- .../components/home_connect/strings.json | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index ca79ec56ee4..19d7cc06046 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1551,31 +1551,39 @@ } }, "coffee_counter": { - "name": "Coffees" + "name": "Coffees", + "unit_of_measurement": "coffees" }, "powder_coffee_counter": { - "name": "Powder coffees" + "name": "Powder coffees", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::coffee_counter::unit_of_measurement%]" }, "hot_water_counter": { "name": "Hot water" }, "hot_water_cups_counter": { - "name": "Hot water cups" + "name": "Hot water cups", + "unit_of_measurement": "cups" }, "hot_milk_counter": { - "name": "Hot milk cups" + "name": "Hot milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "frothy_milk_counter": { - "name": "Frothy milk cups" + "name": "Frothy milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "milk_counter": { - "name": "Milk cups" + "name": "Milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "coffee_and_milk_counter": { - "name": "Coffee and milk cups" + "name": "Coffee and milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "ristretto_espresso_counter": { - "name": "Ristretto espresso cups" + "name": "Ristretto espresso cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "battery_level": { "name": "Battery level" From 9ea3e786f68575a3c507aa0dec0b9173ce9ceb41 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 30 Apr 2025 22:56:04 +0200 Subject: [PATCH 004/110] Bump pylamarzocco to 2.0.0b7 (#143989) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 7d554214fee..ab5a77cad4c 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.0b6"] + "requirements": ["pylamarzocco==2.0.0b7"] } diff --git a/requirements_all.txt b/requirements_all.txt index b32ecebfea2..235605bad07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b6 +pylamarzocco==2.0.0b7 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2590b02ed27..75c80f5180f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b6 +pylamarzocco==2.0.0b7 # homeassistant.components.lastfm pylast==5.1.0 From 9293afd95aa0c93ccc0d8b25ec8fcd311f7d74e7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Apr 2025 16:57:02 -0400 Subject: [PATCH 005/110] Ensure legacy TTS providers are hidden if entity exists (#143992) --- homeassistant/components/cloud/tts.py | 10 +++-- homeassistant/components/tts/__init__.py | 3 ++ homeassistant/components/tts/legacy.py | 1 + homeassistant/components/tts/media_source.py | 21 +++++++---- tests/components/tts/test_init.py | 39 ++++++++++++++++++++ tests/components/tts/test_media_source.py | 7 ++++ 6 files changed, 71 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index ca3e0719998..85ca599fa87 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -418,9 +418,11 @@ class CloudTTSEntity(TextToSpeechEntity): language=language, voice=options.get( ATTR_VOICE, - self._voice - if language == self._language - else DEFAULT_VOICES[language], + ( + self._voice + if language == self._language + else DEFAULT_VOICES[language] + ), ), gender=options.get(ATTR_GENDER), ), @@ -435,6 +437,8 @@ class CloudTTSEntity(TextToSpeechEntity): class CloudProvider(Provider): """Home Assistant Cloud speech API provider.""" + has_entity = True + def __init__(self, cloud: Cloud[CloudClient]) -> None: """Initialize cloud provider.""" self.cloud = cloud diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 44badaa73d2..b279af31803 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1212,6 +1212,9 @@ def websocket_list_engines( if entity.platform: entity_domains.add(entity.platform.platform_name) for engine_id, provider in hass.data[DATA_TTS_MANAGER].providers.items(): + if provider.has_entity: + continue + provider_info = { "engine_id": engine_id, "name": provider.name, diff --git a/homeassistant/components/tts/legacy.py b/homeassistant/components/tts/legacy.py index 6f0541734d1..877ecc034d6 100644 --- a/homeassistant/components/tts/legacy.py +++ b/homeassistant/components/tts/legacy.py @@ -207,6 +207,7 @@ class Provider: hass: HomeAssistant | None = None name: str | None = None + has_entity: bool = False @property def default_language(self) -> str | None: diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index 97d2ab549bc..d3c0998bb77 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -145,13 +145,20 @@ class TTSMediaSource(MediaSource): return self._engine_item(engine, params) # Root. List providers. - children = [ - self._engine_item(engine) - for engine in self.hass.data[DATA_TTS_MANAGER].providers - ] + [ - self._engine_item(entity.entity_id) - for entity in self.hass.data[DATA_COMPONENT].entities - ] + children = sorted( + [ + self._engine_item(engine_id) + for engine_id, provider in self.hass.data[ + DATA_TTS_MANAGER + ].providers.items() + if not provider.has_entity + ] + + [ + self._engine_item(entity.entity_id) + for entity in self.hass.data[DATA_COMPONENT].entities + ], + key=lambda x: x.title, + ) return BrowseMediaSource( domain=DOMAIN, identifier=None, diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 45424be8481..ea281506f3a 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1522,6 +1522,45 @@ async def test_fetching_in_async( ) +@pytest.mark.parametrize( + ("setup", "engine_id"), + [ + ("mock_setup", "test"), + ], + indirect=["setup"], +) +async def test_ws_list_engines_filter_deprecated( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup: str, + engine_id: str, +) -> None: + """Test listing tts engines and supported languages.""" + client = await hass_ws_client() + + await client.send_json_auto_id({"type": "tts/engine/list"}) + + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "providers": [ + { + "name": "Test", + "engine_id": engine_id, + "supported_languages": ["de_CH", "de_DE", "en_GB", "en_US"], + } + ] + } + + hass.data[tts.DATA_TTS_MANAGER].providers[engine_id].has_entity = True + + await client.send_json_auto_id({"type": "tts/engine/list"}) + + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"providers": []} + + @pytest.mark.parametrize( ("setup", "engine_id", "extra_data"), [ diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index 4ff0a44a4bb..c9d70c7f43e 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -114,6 +114,13 @@ async def test_legacy_resolving( await mock_setup(hass, mock_provider) mock_get_tts_audio = mock_provider.get_tts_audio + mock_provider.has_entity = True + root = await media_source.async_browse_media(hass, "media-source://tts") + assert len(root.children) == 0 + mock_provider.has_entity = False + root = await media_source.async_browse_media(hass, "media-source://tts") + assert len(root.children) == 1 + mock_get_tts_audio.reset_mock() media_id = "media-source://tts/test?message=Hello%20World" media = await media_source.async_resolve_media(hass, media_id, None) From e82713b68cec08d78c6a882248c39d211b286d85 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 30 Apr 2025 23:13:04 +0200 Subject: [PATCH 006/110] Add translations for "energy_distance" and "wind_direction" in `random` (#143994) * Add translations for "energy_distance" and "wind_direction" in `random` * Comma --- homeassistant/components/random/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index bacd6dd5a17..af0efb823b9 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -98,6 +98,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", @@ -134,6 +135,7 @@ "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } } From 99a0679ee98f687927b30b5b2d9db62cc965a64a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 30 Apr 2025 23:45:41 +0200 Subject: [PATCH 007/110] Default backup encryption to true when updating only location retention (#143997) --- homeassistant/components/backup/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/backup/config.py b/homeassistant/components/backup/config.py index 75576105e92..0c8a5c82f7c 100644 --- a/homeassistant/components/backup/config.py +++ b/homeassistant/components/backup/config.py @@ -202,7 +202,7 @@ class BackupConfig: if agent_id not in self.data.agents: old_agent_retention = None self.data.agents[agent_id] = AgentConfig( - protected=agent_config.get("protected", False), + protected=agent_config.get("protected", True), retention=new_agent_retention, ) else: From 0cbeeebd0b6a13b0314b388665705d7232e399ed Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 1 May 2025 22:29:44 +0200 Subject: [PATCH 008/110] Add connect/disconnect callbacks to lamarzocco (#144011) --- homeassistant/components/lamarzocco/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index cfe570efb53..751ef550516 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -97,14 +97,15 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): self.config_entry.async_create_background_task( hass=self.hass, target=self.device.connect_dashboard_websocket( - update_callback=lambda _: self.async_set_updated_data(None) + update_callback=lambda _: self.async_set_updated_data(None), + connect_callback=self.async_update_listeners, + disconnect_callback=self.async_update_listeners, ), name="lm_websocket_task", ) async def websocket_close(_: Any | None = None) -> None: - if self.device.websocket.connected: - await self.device.websocket.disconnect() + await self.device.websocket.disconnect() self.config_entry.async_on_unload( self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, websocket_close) From 3fbd23b98dec03693be73263bcbe2cdfb01c801c Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 1 May 2025 22:26:50 +0200 Subject: [PATCH 009/110] Add bluetooth connection availability to diagnostics for lamarzocco (#144012) * Add bluetooth connection availability to diagnostics for lamarzocco * make even more detailed --- .../components/lamarzocco/diagnostics.py | 12 +- .../snapshots/test_diagnostics.ambr | 1057 +++++++++-------- 2 files changed, 543 insertions(+), 526 deletions(-) diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 6837dd6a9ee..7743523e01d 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -5,8 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_MAC, CONF_TOKEN from homeassistant.core import HomeAssistant +from .const import CONF_USE_BLUETOOTH from .coordinator import LaMarzoccoConfigEntry TO_REDACT = { @@ -21,4 +23,12 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator = entry.runtime_data.config_coordinator device = coordinator.device - return async_redact_data(device.to_dict(), TO_REDACT) + data = { + "device": device.to_dict(), + "bluetooth_available": { + "options_enabled": entry.options.get(CONF_USE_BLUETOOTH, True), + CONF_MAC: CONF_MAC in entry.data, + CONF_TOKEN: CONF_TOKEN in entry.data, + }, + } + return async_redact_data(data, TO_REDACT) diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index 31292862824..33b4b4092f7 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -1,309 +1,22 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'dashboard': dict({ - 'available_firmware_update': False, - 'ble_auth_token': None, - 'coffee_station': None, - 'config': dict({ - 'CMBackFlush': dict({ - 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', - 'status': 'Off', - }), - 'CMCoffeeBoiler': dict({ - 'enabled': True, - 'enabled_supported': False, - 'ready_start_time': None, - 'status': 'Ready', - 'target_temperature': 95.0, - 'target_temperature_max': 110, - 'target_temperature_min': 80, - 'target_temperature_step': 0.1, - }), - 'CMGroupDoses': dict({ - 'available_modes': list([ - 'PulsesType', - ]), - 'brewing_pressure': None, - 'brewing_pressure_supported': False, - 'continuous_dose': None, - 'continuous_dose_supported': False, - 'doses': dict({ - 'pulses_type': list([ - dict({ - 'dose': 126.0, - 'dose_index': 'DoseA', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - dict({ - 'dose': 126.0, - 'dose_index': 'DoseB', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - dict({ - 'dose': 160.0, - 'dose_index': 'DoseC', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - dict({ - 'dose': 77.0, - 'dose_index': 'DoseD', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - ]), + 'bluetooth_available': dict({ + 'mac': False, + 'options_enabled': True, + 'token': True, + }), + 'device': dict({ + 'dashboard': dict({ + 'available_firmware_update': False, + 'ble_auth_token': None, + 'coffee_station': None, + 'config': dict({ + 'CMBackFlush': dict({ + 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', + 'status': 'Off', }), - 'mirror_with_group_1': None, - 'mirror_with_group_1_not_effective': False, - 'mirror_with_group_1_supported': False, - 'mode': 'PulsesType', - 'profile': None, - }), - 'CMHotWaterDose': dict({ - 'doses': list([ - dict({ - 'dose': 8.0, - 'dose_index': 'DoseA', - 'dose_max': 90.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - ]), - 'enabled': True, - 'enabled_supported': False, - }), - 'CMMachineStatus': dict({ - 'available_modes': list([ - 'BrewingMode', - 'StandBy', - ]), - 'brewing_start_time': None, - 'mode': 'BrewingMode', - 'next_status': dict({ - 'start_time': '2025-03-24T22:59:55.332000+00:00', - 'status': 'StandBy', - }), - 'status': 'PoweredOn', - }), - 'CMPreBrewing': dict({ - 'available_modes': list([ - 'PreBrewing', - 'PreInfusion', - 'Disabled', - ]), - 'dose_index_supported': True, - 'mode': 'PreInfusion', - 'times': dict({ - 'pre_brewing': list([ - dict({ - 'dose_index': 'DoseA', - 'seconds': dict({ - 'In': 0.5, - 'Out': 1.0, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseB', - 'seconds': dict({ - 'In': 0.5, - 'Out': 1.0, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseC', - 'seconds': dict({ - 'In': 3.3, - 'Out': 3.3, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseD', - 'seconds': dict({ - 'In': 2.0, - 'Out': 2.0, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - ]), - 'pre_infusion': list([ - dict({ - 'dose_index': 'DoseA', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseB', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseC', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseD', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - ]), - }), - }), - 'CMSteamBoilerTemperature': dict({ - 'enabled': True, - 'enabled_supported': True, - 'ready_start_time': None, - 'status': 'Off', - 'target_temperature': 123.9, - 'target_temperature_max': 140, - 'target_temperature_min': 95, - 'target_temperature_step': 0.1, - 'target_temperature_supported': True, - }), - }), - 'connected': True, - 'connection_date': '2025-03-20T16:44:47.479000+00:00', - 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png', - 'location': 'HOME', - 'model_code': 'GS3AV', - 'model_name': 'GS3 AV', - 'name': 'GS012345', - 'offline_mode': False, - 'require_firmware_update': False, - 'serial_number': '**REDACTED**', - 'type': 'CoffeeMachine', - 'widgets': list([ - dict({ - 'code': 'CMMachineStatus', - 'index': 1, - 'output': dict({ - 'available_modes': list([ - 'BrewingMode', - 'StandBy', - ]), - 'brewing_start_time': None, - 'mode': 'BrewingMode', - 'next_status': dict({ - 'start_time': '2025-03-24T22:59:55.332000+00:00', - 'status': 'StandBy', - }), - 'status': 'PoweredOn', - }), - }), - dict({ - 'code': 'CMCoffeeBoiler', - 'index': 1, - 'output': dict({ + 'CMCoffeeBoiler': dict({ 'enabled': True, 'enabled_supported': False, 'ready_start_time': None, @@ -313,26 +26,7 @@ 'target_temperature_min': 80, 'target_temperature_step': 0.1, }), - }), - dict({ - 'code': 'CMSteamBoilerTemperature', - 'index': 1, - 'output': dict({ - 'enabled': True, - 'enabled_supported': True, - 'ready_start_time': None, - 'status': 'Off', - 'target_temperature': 123.9, - 'target_temperature_max': 140, - 'target_temperature_min': 95, - 'target_temperature_step': 0.1, - 'target_temperature_supported': True, - }), - }), - dict({ - 'code': 'CMGroupDoses', - 'index': 1, - 'output': dict({ + 'CMGroupDoses': dict({ 'available_modes': list([ 'PulsesType', ]), @@ -378,11 +72,33 @@ 'mode': 'PulsesType', 'profile': None, }), - }), - dict({ - 'code': 'CMPreBrewing', - 'index': 1, - 'output': dict({ + 'CMHotWaterDose': dict({ + 'doses': list([ + dict({ + 'dose': 8.0, + 'dose_index': 'DoseA', + 'dose_max': 90.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + ]), + 'enabled': True, + 'enabled_supported': False, + }), + 'CMMachineStatus': dict({ + 'available_modes': list([ + 'BrewingMode', + 'StandBy', + ]), + 'brewing_start_time': None, + 'mode': 'BrewingMode', + 'next_status': dict({ + 'start_time': '2025-03-24T22:59:55.332000+00:00', + 'status': 'StandBy', + }), + 'status': 'PoweredOn', + }), + 'CMPreBrewing': dict({ 'available_modes': list([ 'PreBrewing', 'PreInfusion', @@ -549,218 +265,509 @@ ]), }), }), - }), - dict({ - 'code': 'CMHotWaterDose', - 'index': 1, - 'output': dict({ - 'doses': list([ - dict({ - 'dose': 8.0, - 'dose_index': 'DoseA', - 'dose_max': 90.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - ]), + 'CMSteamBoilerTemperature': dict({ 'enabled': True, - 'enabled_supported': False, - }), - }), - dict({ - 'code': 'CMBackFlush', - 'index': 1, - 'output': dict({ - 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', + 'enabled_supported': True, + 'ready_start_time': None, 'status': 'Off', + 'target_temperature': 123.9, + 'target_temperature_max': 140, + 'target_temperature_min': 95, + 'target_temperature_step': 0.1, + 'target_temperature_supported': True, }), }), - ]), - }), - 'schedule': dict({ - 'available_firmware_update': False, - 'ble_auth_token': None, - 'coffee_station': None, - 'connected': True, - 'connection_date': '2025-03-21T03:00:19.892000+00:00', - 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', - 'location': None, - 'model_code': 'LINEAMICRA', - 'model_name': 'Linea Micra', - 'name': 'MR123456', - 'offline_mode': False, - 'require_firmware_update': False, - 'serial_number': '**REDACTED**', - 'smart_wake_up_sleep': dict({ - 'schedules': list([ + 'connected': True, + 'connection_date': '2025-03-20T16:44:47.479000+00:00', + 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png', + 'location': 'HOME', + 'model_code': 'GS3AV', + 'model_name': 'GS3 AV', + 'name': 'GS012345', + 'offline_mode': False, + 'require_firmware_update': False, + 'serial_number': '**REDACTED**', + 'type': 'CoffeeMachine', + 'widgets': list([ dict({ - 'days': list([ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday', - ]), - 'enabled': True, - 'id': 'Os2OswX', - 'offTimeMinutes': 1440, - 'onTimeMinutes': 1320, - 'steamBoiler': True, + 'code': 'CMMachineStatus', + 'index': 1, + 'output': dict({ + 'available_modes': list([ + 'BrewingMode', + 'StandBy', + ]), + 'brewing_start_time': None, + 'mode': 'BrewingMode', + 'next_status': dict({ + 'start_time': '2025-03-24T22:59:55.332000+00:00', + 'status': 'StandBy', + }), + 'status': 'PoweredOn', + }), }), dict({ - 'days': list([ - 'Sunday', - ]), - 'enabled': True, - 'id': 'aXFz5bJ', - 'offTimeMinutes': 450, - 'onTimeMinutes': 420, - 'steamBoiler': False, + 'code': 'CMCoffeeBoiler', + 'index': 1, + 'output': dict({ + 'enabled': True, + 'enabled_supported': False, + 'ready_start_time': None, + 'status': 'Ready', + 'target_temperature': 95.0, + 'target_temperature_max': 110, + 'target_temperature_min': 80, + 'target_temperature_step': 0.1, + }), + }), + dict({ + 'code': 'CMSteamBoilerTemperature', + 'index': 1, + 'output': dict({ + 'enabled': True, + 'enabled_supported': True, + 'ready_start_time': None, + 'status': 'Off', + 'target_temperature': 123.9, + 'target_temperature_max': 140, + 'target_temperature_min': 95, + 'target_temperature_step': 0.1, + 'target_temperature_supported': True, + }), + }), + dict({ + 'code': 'CMGroupDoses', + 'index': 1, + 'output': dict({ + 'available_modes': list([ + 'PulsesType', + ]), + 'brewing_pressure': None, + 'brewing_pressure_supported': False, + 'continuous_dose': None, + 'continuous_dose_supported': False, + 'doses': dict({ + 'pulses_type': list([ + dict({ + 'dose': 126.0, + 'dose_index': 'DoseA', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + dict({ + 'dose': 126.0, + 'dose_index': 'DoseB', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + dict({ + 'dose': 160.0, + 'dose_index': 'DoseC', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + dict({ + 'dose': 77.0, + 'dose_index': 'DoseD', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + ]), + }), + 'mirror_with_group_1': None, + 'mirror_with_group_1_not_effective': False, + 'mirror_with_group_1_supported': False, + 'mode': 'PulsesType', + 'profile': None, + }), + }), + dict({ + 'code': 'CMPreBrewing', + 'index': 1, + 'output': dict({ + 'available_modes': list([ + 'PreBrewing', + 'PreInfusion', + 'Disabled', + ]), + 'dose_index_supported': True, + 'mode': 'PreInfusion', + 'times': dict({ + 'pre_brewing': list([ + dict({ + 'dose_index': 'DoseA', + 'seconds': dict({ + 'In': 0.5, + 'Out': 1.0, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseB', + 'seconds': dict({ + 'In': 0.5, + 'Out': 1.0, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseC', + 'seconds': dict({ + 'In': 3.3, + 'Out': 3.3, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseD', + 'seconds': dict({ + 'In': 2.0, + 'Out': 2.0, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + ]), + 'pre_infusion': list([ + dict({ + 'dose_index': 'DoseA', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseB', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseC', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseD', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + ]), + }), + }), + }), + dict({ + 'code': 'CMHotWaterDose', + 'index': 1, + 'output': dict({ + 'doses': list([ + dict({ + 'dose': 8.0, + 'dose_index': 'DoseA', + 'dose_max': 90.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + ]), + 'enabled': True, + 'enabled_supported': False, + }), + }), + dict({ + 'code': 'CMBackFlush', + 'index': 1, + 'output': dict({ + 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', + 'status': 'Off', + }), }), ]), - 'schedules_dict': dict({ - 'Os2OswX': dict({ - 'days': list([ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday', - ]), - 'enabled': True, - 'id': 'Os2OswX', - 'offTimeMinutes': 1440, - 'onTimeMinutes': 1320, - 'steamBoiler': True, - }), - 'aXFz5bJ': dict({ - 'days': list([ - 'Sunday', - ]), - 'enabled': True, - 'id': 'aXFz5bJ', - 'offTimeMinutes': 450, - 'onTimeMinutes': 420, - 'steamBoiler': False, - }), - }), - 'smart_stand_by_after': 'PowerOn', - 'smart_stand_by_enabled': True, - 'smart_stand_by_minutes': 10, - 'smart_stand_by_minutes_max': 30, - 'smart_stand_by_minutes_min': 1, - 'smart_stand_by_minutes_step': 1, }), - 'smart_wake_up_sleep_supported': True, - 'type': 'CoffeeMachine', - }), - 'serial_number': '**REDACTED**', - 'settings': dict({ - 'actual_firmwares': list([ - dict({ - 'available_update': dict({ - 'build_version': 'v5.0.10', - 'change_log': ''' - What’s new in this version: - - * fixed an issue that could cause the machine powers up outside scheduled time - * minor improvements - ''', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', + 'schedule': dict({ + 'available_firmware_update': False, + 'ble_auth_token': None, + 'coffee_station': None, + 'connected': True, + 'connection_date': '2025-03-21T03:00:19.892000+00:00', + 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', + 'location': None, + 'model_code': 'LINEAMICRA', + 'model_name': 'Linea Micra', + 'name': 'MR123456', + 'offline_mode': False, + 'require_firmware_update': False, + 'serial_number': '**REDACTED**', + 'smart_wake_up_sleep': dict({ + 'schedules': list([ + dict({ + 'days': list([ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]), + 'enabled': True, + 'id': 'Os2OswX', + 'offTimeMinutes': 1440, + 'onTimeMinutes': 1320, + 'steamBoiler': True, + }), + dict({ + 'days': list([ + 'Sunday', + ]), + 'enabled': True, + 'id': 'aXFz5bJ', + 'offTimeMinutes': 450, + 'onTimeMinutes': 420, + 'steamBoiler': False, + }), + ]), + 'schedules_dict': dict({ + 'Os2OswX': dict({ + 'days': list([ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]), + 'enabled': True, + 'id': 'Os2OswX', + 'offTimeMinutes': 1440, + 'onTimeMinutes': 1320, + 'steamBoiler': True, + }), + 'aXFz5bJ': dict({ + 'days': list([ + 'Sunday', + ]), + 'enabled': True, + 'id': 'aXFz5bJ', + 'offTimeMinutes': 450, + 'onTimeMinutes': 420, + 'steamBoiler': False, + }), }), - 'build_version': 'v5.0.9', - 'change_log': ''' - What’s new in this version: - - * New La Marzocco compatibility - * Improved connectivity - * Improved pairing process - * Improved statistics - * Boilers heating time - * Last backflush date (GS3 MP excluded) - * Automatic gateway updates option - ''', - 'status': 'ToUpdate', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', - }), - dict({ - 'available_update': None, - 'build_version': 'v1.17', - 'change_log': 'None', - 'status': 'Updated', - 'thing_model_code': 'LineaMicra', - 'type': 'Machine', - }), - ]), - 'auto_update': False, - 'auto_update_supported': True, - 'available_firmware_update': False, - 'ble_auth_token': None, - 'coffee_station': None, - 'connected': True, - 'connection_date': '2025-03-21T03:00:19.892000+00:00', - 'cropster_active': False, - 'cropster_supported': False, - 'factory_reset_supported': True, - 'firmwares': dict({ - 'Gateway': dict({ - 'available_update': dict({ - 'build_version': 'v5.0.10', - 'change_log': ''' - What’s new in this version: - - * fixed an issue that could cause the machine powers up outside scheduled time - * minor improvements - ''', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', - }), - 'build_version': 'v5.0.9', - 'change_log': ''' - What’s new in this version: - - * New La Marzocco compatibility - * Improved connectivity - * Improved pairing process - * Improved statistics - * Boilers heating time - * Last backflush date (GS3 MP excluded) - * Automatic gateway updates option - ''', - 'status': 'ToUpdate', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', - }), - 'Machine': dict({ - 'available_update': None, - 'build_version': 'v1.17', - 'change_log': 'None', - 'status': 'Updated', - 'thing_model_code': 'LineaMicra', - 'type': 'Machine', + 'smart_stand_by_after': 'PowerOn', + 'smart_stand_by_enabled': True, + 'smart_stand_by_minutes': 10, + 'smart_stand_by_minutes_max': 30, + 'smart_stand_by_minutes_min': 1, + 'smart_stand_by_minutes_step': 1, }), + 'smart_wake_up_sleep_supported': True, + 'type': 'CoffeeMachine', }), - 'hemro_active': False, - 'hemro_supported': False, - 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', - 'is_plumbed_in': True, - 'location': None, - 'model_code': 'LINEAMICRA', - 'model_name': 'Linea Micra', - 'name': 'MR123456', - 'offline_mode': False, - 'plumb_in_supported': True, - 'require_firmware_update': False, 'serial_number': '**REDACTED**', - 'type': 'CoffeeMachine', - 'wifi_rssi': -51, - 'wifi_ssid': 'MyWifi', + 'settings': dict({ + 'actual_firmwares': list([ + dict({ + 'available_update': dict({ + 'build_version': 'v5.0.10', + 'change_log': ''' + What’s new in this version: + + * fixed an issue that could cause the machine powers up outside scheduled time + * minor improvements + ''', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + 'build_version': 'v5.0.9', + 'change_log': ''' + What’s new in this version: + + * New La Marzocco compatibility + * Improved connectivity + * Improved pairing process + * Improved statistics + * Boilers heating time + * Last backflush date (GS3 MP excluded) + * Automatic gateway updates option + ''', + 'status': 'ToUpdate', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + dict({ + 'available_update': None, + 'build_version': 'v1.17', + 'change_log': 'None', + 'status': 'Updated', + 'thing_model_code': 'LineaMicra', + 'type': 'Machine', + }), + ]), + 'auto_update': False, + 'auto_update_supported': True, + 'available_firmware_update': False, + 'ble_auth_token': None, + 'coffee_station': None, + 'connected': True, + 'connection_date': '2025-03-21T03:00:19.892000+00:00', + 'cropster_active': False, + 'cropster_supported': False, + 'factory_reset_supported': True, + 'firmwares': dict({ + 'Gateway': dict({ + 'available_update': dict({ + 'build_version': 'v5.0.10', + 'change_log': ''' + What’s new in this version: + + * fixed an issue that could cause the machine powers up outside scheduled time + * minor improvements + ''', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + 'build_version': 'v5.0.9', + 'change_log': ''' + What’s new in this version: + + * New La Marzocco compatibility + * Improved connectivity + * Improved pairing process + * Improved statistics + * Boilers heating time + * Last backflush date (GS3 MP excluded) + * Automatic gateway updates option + ''', + 'status': 'ToUpdate', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + 'Machine': dict({ + 'available_update': None, + 'build_version': 'v1.17', + 'change_log': 'None', + 'status': 'Updated', + 'thing_model_code': 'LineaMicra', + 'type': 'Machine', + }), + }), + 'hemro_active': False, + 'hemro_supported': False, + 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', + 'is_plumbed_in': True, + 'location': None, + 'model_code': 'LINEAMICRA', + 'model_name': 'Linea Micra', + 'name': 'MR123456', + 'offline_mode': False, + 'plumb_in_supported': True, + 'require_firmware_update': False, + 'serial_number': '**REDACTED**', + 'type': 'CoffeeMachine', + 'wifi_rssi': -51, + 'wifi_ssid': 'MyWifi', + }), }), }) # --- From 43b737c4a2d6fc36744981fd827e6baede7ad7df Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 1 May 2025 13:47:48 -0700 Subject: [PATCH 010/110] Pass empty set instead of empty dict to get_last_statistics (#144022) --- homeassistant/components/opower/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index d0e95b27ec3..adb32d914ee 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -349,7 +349,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): 1, target_id, True, - {}, + set(), ) if not last_target_stat: need_migration_source_ids.add(source_id) From 23ba652b83b55fc6808d82791431978521ff4a62 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Thu, 1 May 2025 10:42:27 +0200 Subject: [PATCH 011/110] Fix state of fan entity for Miele hobs with extractor when turned off (#144025) --- homeassistant/components/miele/fan.py | 6 +- .../miele/fixtures/fan_devices.json | 124 ++++++++++++++++++ .../components/miele/snapshots/test_fan.ambr | 49 +++++++ 3 files changed, 177 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/miele/fan.py b/homeassistant/components/miele/fan.py index 4781d27901f..fcd74a93bfb 100644 --- a/homeassistant/components/miele/fan.py +++ b/homeassistant/components/miele/fan.py @@ -99,8 +99,10 @@ class MieleFan(MieleEntity, FanEntity): @property def is_on(self) -> bool: """Return current on/off state.""" - assert self.device.state_ventilation_step is not None - return self.device.state_ventilation_step > 0 + return ( + self.device.state_ventilation_step is not None + and self.device.state_ventilation_step > 0 + ) @property def speed_count(self) -> int: diff --git a/tests/components/miele/fixtures/fan_devices.json b/tests/components/miele/fixtures/fan_devices.json index d3403c0f7bc..9904f6f5faa 100644 --- a/tests/components/miele/fixtures/fan_devices.json +++ b/tests/components/miele/fixtures/fan_devices.json @@ -210,5 +210,129 @@ "ecoFeedback": null, "batteryLevel": null } + }, + "DummyAppliance_74_off": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 74, + "value_localized": "Hob with vapour extraction" + }, + "deviceName": "", + "protocolVersion": 2, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "00", + "techType": "KMDA7473", + "matNumber": "", + "swids": ["000"] + }, + "xkmIdentLabel": { + "techType": "EK039W", + "releaseVersion": "02.80" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": false, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } } } diff --git a/tests/components/miele/snapshots/test_fan.ambr b/tests/components/miele/snapshots/test_fan.ambr index ffd6c90a388..595d4463462 100644 --- a/tests/components/miele/snapshots/test_fan.ambr +++ b/tests/components/miele/snapshots/test_fan.ambr @@ -48,6 +48,55 @@ 'state': 'on', }) # --- +# name: test_fan_states[fan_devices.json-platforms0][fan.hob_with_extraction_fan_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.hob_with_extraction_fan_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fan', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan', + 'unique_id': 'DummyAppliance_74_off-fan_readonly', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_states[fan_devices.json-platforms0][fan.hob_with_extraction_fan_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hob with extraction Fan', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.hob_with_extraction_fan_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_fan_states[fan_devices.json-platforms0][fan.hood_fan-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 52b0b1e2abec8d859d332c37c504eec6c6b6ec46 Mon Sep 17 00:00:00 2001 From: OzGav Date: Thu, 1 May 2025 19:40:04 +1000 Subject: [PATCH 012/110] Media Player strings adjust grammar (#144030) --- homeassistant/components/media_player/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 459b54b8af2..617cb258af7 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -291,7 +291,7 @@ "description": "The term to search for." }, "media_filter_classes": { - "name": "Media filter classes", + "name": "Media class filter", "description": "List of media classes to filter the search results by." } } From 1143468eb5096d310bbbfd457e16aef678c4c5dc Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 1 May 2025 14:02:39 +0200 Subject: [PATCH 013/110] Handle TimeoutError for lamarzocco (#144042) --- homeassistant/components/lamarzocco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 1d77dbc2f1a..ad9fec28fb4 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_failed" ) from ex - except RequestNotSuccessful as ex: + except (RequestNotSuccessful, TimeoutError) as ex: _LOGGER.debug(ex, exc_info=True) raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="api_error" From ea9a0f4bf546f9ae441f97dd79f413f4c44ef602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 1 May 2025 16:06:49 +0200 Subject: [PATCH 014/110] Use action property defined in MieleEntity (#144052) --- homeassistant/components/miele/button.py | 3 +-- homeassistant/components/miele/climate.py | 8 ++------ homeassistant/components/miele/entity.py | 2 +- homeassistant/components/miele/switch.py | 13 ++++--------- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/miele/button.py b/homeassistant/components/miele/button.py index e4aacc5124c..70d4489e9be 100644 --- a/homeassistant/components/miele/button.py +++ b/homeassistant/components/miele/button.py @@ -131,8 +131,7 @@ class MieleButton(MieleEntity, ButtonEntity): return ( super().available - and self.entity_description.press_data - in self.coordinator.data.actions[self._device_id].process_actions + and self.entity_description.press_data in self.action.process_actions ) async def async_press(self) -> None: diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 3b591965d2f..054ab227ca6 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -201,9 +201,7 @@ class MieleClimate(MieleEntity, ClimateEntity): """Return the maximum target temperature.""" return cast( float, - self.coordinator.data.actions[self._device_id] - .target_temperature[self.entity_description.zone - 1] - .max, + self.action.target_temperature[self.entity_description.zone - 1].max, ) @property @@ -211,9 +209,7 @@ class MieleClimate(MieleEntity, ClimateEntity): """Return the minimum target temperature.""" return cast( float, - self.coordinator.data.actions[self._device_id] - .target_temperature[self.entity_description.zone - 1] - .min, + self.action.target_temperature[self.entity_description.zone - 1].min, ) async def async_set_temperature(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/miele/entity.py b/homeassistant/components/miele/entity.py index a84c1f1108b..f9ed4f0bf48 100644 --- a/homeassistant/components/miele/entity.py +++ b/homeassistant/components/miele/entity.py @@ -47,7 +47,7 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]): return self.coordinator.data.devices[self._device_id] @property - def actions(self) -> MieleAction: + def action(self) -> MieleAction: """Return the actions object.""" return self.coordinator.data.actions[self._device_id] diff --git a/homeassistant/components/miele/switch.py b/homeassistant/components/miele/switch.py index 74a9f0c4785..427d90968b7 100644 --- a/homeassistant/components/miele/switch.py +++ b/homeassistant/components/miele/switch.py @@ -169,15 +169,14 @@ class MielePowerSwitch(MieleSwitch): @property def is_on(self) -> bool | None: """Return the state of the switch.""" - return self.coordinator.data.actions[self._device_id].power_off_enabled + return self.action.power_off_enabled @property def available(self) -> bool: """Return the availability of the entity.""" return ( - self.coordinator.data.actions[self._device_id].power_off_enabled - or self.coordinator.data.actions[self._device_id].power_on_enabled + self.action.power_off_enabled or self.action.power_on_enabled ) and super().available async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None: @@ -192,12 +191,8 @@ class MielePowerSwitch(MieleSwitch): "entity": self.entity_id, }, ) from err - self.coordinator.data.actions[self._device_id].power_on_enabled = cast( - bool, mode - ) - self.coordinator.data.actions[self._device_id].power_off_enabled = not cast( - bool, mode - ) + self.action.power_on_enabled = cast(bool, mode) + self.action.power_off_enabled = not cast(bool, mode) self.async_write_ha_state() From 851779e7adb3f5b2503ed7a59d84fd95463635a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 1 May 2025 16:33:30 +0200 Subject: [PATCH 015/110] Use device class transation for door in miele (#144053) --- homeassistant/components/miele/strings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 65a38612afd..032a214d442 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -115,9 +115,6 @@ }, "entity": { "binary_sensor": { - "door": { - "name": "Door" - }, "failure": { "name": "Failure" }, From eba0daa2e9ea93686d303c05e2888fbca341d67d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 May 2025 13:51:38 -0500 Subject: [PATCH 016/110] Avoid validation of ESPHome MAC when discovered entry is ignored or unchanged (#144071) fixes #144033 fixes #143991 --- .../components/esphome/config_flow.py | 10 +++ tests/components/esphome/test_config_flow.py | 66 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index d94ce99c6bf..75408246e78 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -22,6 +22,7 @@ import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ( + SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE, ConfigEntry, @@ -31,6 +32,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo @@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) ): return + if entry.source == SOURCE_IGNORE: + # Don't call _fetch_device_info() for ignored entries + raise AbortFlow("already_configured") + configured_host: str | None = entry.data.get(CONF_HOST) configured_port: int | None = entry.data.get(CONF_PORT) + if configured_host == host and configured_port == port: + # Don't probe to verify the mac is correct since + # the host and port matches. + raise AbortFlow("already_configured") configured_psk: str | None = entry.data.get(CONF_NOISE_PSK) await self._fetch_device_info(host, port or configured_port, configured_psk) updates: dict[str, Any] = {} diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 53abf6fb3ab..ead9167d258 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -27,7 +27,7 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -747,6 +747,35 @@ async def test_discovery_already_configured(hass: HomeAssistant) -> None: } +@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf") +async def test_discovery_ignored(hass: HomeAssistant) -> None: + """Test discovery does not probe and ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + source=SOURCE_IGNORE, + ) + + entry.add_to_hass(hass) + + service_info = ZeroconfServiceInfo( + ip_address=ip_address("192.168.43.183"), + ip_addresses=[ip_address("192.168.43.183")], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={"mac": "1122334455aa"}, + type="mock_type", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf") async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: """Test discovery aborts if same mDNS packet arrives.""" @@ -786,8 +815,8 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: entry.add_to_hass(hass) service_info = ZeroconfServiceInfo( - ip_address=ip_address("192.168.43.183"), - ip_addresses=[ip_address("192.168.43.183")], + ip_address=ip_address("192.168.43.184"), + ip_addresses=[ip_address("192.168.43.184")], hostname="test8266.local.", name="mock_name", port=6053, @@ -806,9 +835,40 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: "mac": "11:22:33:44:55:aa", } + assert entry.data[CONF_HOST] == "192.168.43.184" assert entry.unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf") +async def test_discovery_abort_without_update_same_host_port( + hass: HomeAssistant, +) -> None: + """Test discovery aborts without update when hsot and port are the same.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + ) + + entry.add_to_hass(hass) + + service_info = ZeroconfServiceInfo( + ip_address=ip_address("192.168.43.183"), + ip_addresses=[ip_address("192.168.43.183")], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={"address": "test8266.local", "mac": "1122334455aa"}, + type="mock_type", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") async def test_user_requires_psk(hass: HomeAssistant, mock_client: APIClient) -> None: """Test user step with requiring encryption key.""" From 485522fd766a2d79ba4abc483964477a5c421ee6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 May 2025 15:45:44 -0500 Subject: [PATCH 017/110] Avoid DomainData lookup in ESPHome update platform (#144072) We can get this from entry.runtime_data --- homeassistant/components/esphome/update.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index a92204a80d2..01ac638bdb1 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -29,7 +29,6 @@ from homeassistant.util.enum import try_parse_enum from .const import DOMAIN from .coordinator import ESPHomeDashboardCoordinator from .dashboard import async_get_dashboard -from .domain_data import DomainData from .entity import ( EsphomeEntity, convert_api_error_ha_error, @@ -62,7 +61,7 @@ async def async_setup_entry( if (dashboard := async_get_dashboard(hass)) is None: return - entry_data = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data assert entry_data.device_info is not None device_name = entry_data.device_info.name unsubs: list[CALLBACK_TYPE] = [] From 934be08a59d142d77f3f5c878ce65230b03a1381 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 May 2025 15:23:56 -0500 Subject: [PATCH 018/110] Bump inkbird-ble to 0.16.1 (#144074) I made a mistake in one of the data lengths as I forgot to add the length of the id which is 2 bytes. I really wish vendors would stop putting raw data in this field. changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.16.0...v0.16.1 --- homeassistant/components/inkbird/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 79474f0cc28..38d406da62e 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -34,6 +34,10 @@ "local_name": "ITH-21-B", "connectable": false }, + { + "local_name": "IBS-P02B", + "connectable": false + }, { "local_name": "Ink@IAM-T1", "connectable": true @@ -49,5 +53,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.15.0"] + "requirements": ["inkbird-ble==0.16.1"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 9f3c53731c9..e796625f81c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -376,6 +376,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "inkbird", "local_name": "ITH-21-B", }, + { + "connectable": False, + "domain": "inkbird", + "local_name": "IBS-P02B", + }, { "connectable": True, "domain": "inkbird", diff --git a/requirements_all.txt b/requirements_all.txt index 235605bad07..593ba8bdacd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1239,7 +1239,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.15.0 +inkbird-ble==0.16.1 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75c80f5180f..5b3bb6d0a40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1054,7 +1054,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.15.0 +inkbird-ble==0.16.1 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From e7331633c75473fb13f8d71312fed33a987c33ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 May 2025 21:42:07 +0000 Subject: [PATCH 019/110] Bump version to 2025.5.0b1 --- 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 03c45bc317e..620ad2a1be3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index cf47857c2c7..d483ba2a636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b0" +version = "2025.5.0b1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 8a4f28fa944da42b256b589c96e23f96ffada9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6lsch?= <20746434+andreaskoelsch@users.noreply.github.com> Date: Fri, 2 May 2025 00:07:52 +0200 Subject: [PATCH 020/110] Fix brightness calculation when using brightness_step_pct (#143786) --- homeassistant/components/light/__init__.py | 5 +- tests/components/light/test_init.py | 59 ++++++++++++++++------ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7b548533058..d2869670ba4 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -442,7 +442,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: brightness += params.pop(ATTR_BRIGHTNESS_STEP) else: - brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255) + brightness_pct = round(brightness / 255 * 100) + brightness = round( + (brightness_pct + params.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255 + ) params[ATTR_BRIGHTNESS] = max(0, min(255, brightness)) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 29604ce7595..014e3ec8c35 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -958,21 +958,6 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: _, data = entity1.last_call("turn_on") assert data["brightness"] == 40 # 50 - 10 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": [entity0.entity_id, entity1.entity_id], - "brightness_step_pct": 10, - }, - blocking=True, - ) - - _, data = entity0.last_call("turn_on") - assert data["brightness"] == 116 # 90 + (255 * 0.10) - _, data = entity1.last_call("turn_on") - assert data["brightness"] == 66 # 40 + (255 * 0.10) - await hass.services.async_call( "light", "turn_on", @@ -983,7 +968,49 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: blocking=True, ) - assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off + assert entity0.state == "off" # 40 - 126; brightness is 0, light should turn off + + +async def test_light_brightness_step_pct(hass: HomeAssistant) -> None: + """Test that percentage based brightness steps work as expected.""" + entity = MockLight("Test_0", STATE_ON) + + setup_test_component_platform(hass, light.DOMAIN, [entity]) + + entity.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity.supported_color_modes = None + entity.color_mode = None + entity.brightness = 255 + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.attributes["brightness"] == 255 # 100% + + def reduce_brightness_by_ten_percent(): + return hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [entity.entity_id], + "brightness_step_pct": -10, + }, + blocking=True, + ) + + await reduce_brightness_by_ten_percent() + _, data = entity.last_call("turn_on") + assert round(data["brightness"] / 2.55) == 90 # 100% - 10% = 90% + + await reduce_brightness_by_ten_percent() + _, data = entity.last_call("turn_on") + assert round(data["brightness"] / 2.55) == 80 # 90% - 10% = 80% + + await reduce_brightness_by_ten_percent() + _, data = entity.last_call("turn_on") + assert round(data["brightness"] / 2.55) == 70 # 80% - 10% = 70% @pytest.mark.usefixtures("enable_custom_integrations") From 6b10710484e9f5349b6fe2f023fd280738f7691b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 2 May 2025 19:31:56 +0200 Subject: [PATCH 021/110] Improve naming of miele freezers and fridges (#144062) * Use device class transation * Improve naming of miele freezers and fridges * Address review * Address review comment * Simplify --- homeassistant/components/miele/climate.py | 8 +++++-- .../miele/snapshots/test_climate.ambr | 24 +++++++++---------- tests/components/miele/test_climate.py | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 054ab227ca6..22257448e3a 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -174,6 +174,11 @@ class MieleClimate(MieleEntity, ClimateEntity): t_key = ZONE1_DEVICES.get( cast(MieleAppliance, self.device.device_type), "zone_1" ) + if self.device.device_type in ( + MieleAppliance.FRIDGE, + MieleAppliance.FREEZER, + ): + self._attr_name = None if description.zone == 2: if self.device.device_type in ( @@ -192,8 +197,7 @@ class MieleClimate(MieleEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the target temperature.""" - if self.entity_description.target_fn(self.device) is None: - return None + return cast(float | None, self.entity_description.target_fn(self.device)) @property diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 15490047d36..85f7bf212f5 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_climate_states[platforms0-freezer][climate.freezer_freezer-entry] +# name: test_climate_states[platforms0-freezer][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -19,7 +19,7 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.freezer_freezer', + 'entity_id': 'climate.freezer', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,7 +31,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Freezer', + 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, 'supported_features': , @@ -40,11 +40,11 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.freezer_freezer-state] +# name: test_climate_states[platforms0-freezer][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, - 'friendly_name': 'Freezer Freezer', + 'friendly_name': 'Freezer', 'hvac_modes': list([ , ]), @@ -55,14 +55,14 @@ 'temperature': -18, }), 'context': , - 'entity_id': 'climate.freezer_freezer', + 'entity_id': 'climate.freezer', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'cool', }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator_refrigerator-entry] +# name: test_climate_states[platforms0-freezer][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -82,7 +82,7 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.refrigerator_refrigerator', + 'entity_id': 'climate.refrigerator', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -94,7 +94,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Refrigerator', + 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, 'supported_features': , @@ -103,11 +103,11 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator_refrigerator-state] +# name: test_climate_states[platforms0-freezer][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, - 'friendly_name': 'Refrigerator Refrigerator', + 'friendly_name': 'Refrigerator', 'hvac_modes': list([ , ]), @@ -118,7 +118,7 @@ 'temperature': 4, }), 'context': , - 'entity_id': 'climate.refrigerator_refrigerator', + 'entity_id': 'climate.refrigerator', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index 73e530eb87c..f03edada841 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -26,7 +26,7 @@ pytestmark = [ ), ] -ENTITY_ID = "climate.freezer_freezer" +ENTITY_ID = "climate.freezer" SERVICE_SET_TEMPERATURE = "set_temperature" From 628d99886a0e20389858b6a4146750e9df7d71a8 Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 2 May 2025 11:17:58 -0700 Subject: [PATCH 022/110] Bump py-nextbusnext to 2.1.2 (#144081)r Bump py-nextbusnext version Fixes #144059 --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 6300dc1cdc9..a4f6d54f58c 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==2.0.5"] + "requirements": ["py-nextbusnext==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 593ba8bdacd..a98af3d7e0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1759,7 +1759,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.5 +py-nextbusnext==2.1.2 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b3bb6d0a40..d5570b135e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1461,7 +1461,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.5 +py-nextbusnext==2.1.2 # homeassistant.components.nightscout py-nightscout==1.2.2 From e1a908c8acb4c1111730b938813b9dafe21856de Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 3 May 2025 05:13:12 +1000 Subject: [PATCH 023/110] Bump teslemetry-stream to 0.7.7 (#144085) --- homeassistant/components/teslemetry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 8194fb3d6db..5b7454b87b6 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.5"] + "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index a98af3d7e0a..d431b80fba8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2897,7 +2897,7 @@ tesla-powerwall==0.5.2 tesla-wall-connector==1.0.2 # homeassistant.components.teslemetry -teslemetry-stream==0.7.5 +teslemetry-stream==0.7.7 # homeassistant.components.tessie tessie-api==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5570b135e2..28a3a286270 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2341,7 +2341,7 @@ tesla-powerwall==0.5.2 tesla-wall-connector==1.0.2 # homeassistant.components.teslemetry -teslemetry-stream==0.7.5 +teslemetry-stream==0.7.7 # homeassistant.components.tessie tessie-api==0.1.1 From a34065ee2f2b0871dde90ebf702f2b0e4aaf368a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 13:43:06 -0500 Subject: [PATCH 024/110] Only create a single resolver object if there are multiple aiohttp sessions (#144090) --- homeassistant/helpers/aiohttp_client.py | 36 ++++++++++++++++++++++--- tests/conftest.py | 4 ++- tests/helpers/test_aiohttp_client.py | 12 +++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 3d8dc247857..a9976cf7e32 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -28,6 +28,7 @@ from homeassistant.util.json import json_loads from .frame import warn_use from .json import json_dumps +from .singleton import singleton if TYPE_CHECKING: from aiohttp.typedefs import JSONDecoder @@ -39,6 +40,7 @@ DATA_CONNECTOR: HassKey[dict[tuple[bool, int, str], aiohttp.BaseConnector]] = Ha DATA_CLIENTSESSION: HassKey[dict[tuple[bool, int, str], aiohttp.ClientSession]] = ( HassKey("aiohttp_clientsession") ) +DATA_RESOLVER: HassKey[HassAsyncDNSResolver] = HassKey("aiohttp_resolver") SERVER_SOFTWARE = ( f"{APPLICATION_NAME}/{__version__} " @@ -70,6 +72,21 @@ MAXIMUM_CONNECTIONS = 4096 MAXIMUM_CONNECTIONS_PER_HOST = 100 +class HassAsyncDNSResolver(AsyncDualMDNSResolver): + """Home Assistant AsyncDNSResolver. + + This is a wrapper around the AsyncDualMDNSResolver to only + close the resolver when the Home Assistant instance is closed. + """ + + async def real_close(self) -> None: + """Close the resolver.""" + await super().close() + + async def close(self) -> None: + """Close the resolver.""" + + class HassClientResponse(aiohttp.ClientResponse): """aiohttp.ClientResponse with a json method that uses json_loads by default.""" @@ -363,7 +380,7 @@ def _async_get_connector( ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, - resolver=_async_make_resolver(hass), + resolver=_async_get_or_create_resolver(hass), ) connectors[connector_key] = connector @@ -376,6 +393,19 @@ def _async_get_connector( return connector +@singleton(DATA_RESOLVER) @callback -def _async_make_resolver(hass: HomeAssistant) -> AsyncDualMDNSResolver: - return AsyncDualMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) +def _async_get_or_create_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver: + """Return the HassAsyncDNSResolver.""" + resolver = _async_make_resolver(hass) + + async def _async_close_resolver(event: Event) -> None: + await resolver.real_close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_resolver) + return resolver + + +@callback +def _async_make_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver: + return HassAsyncDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) diff --git a/tests/conftest.py b/tests/conftest.py index ff4a09096e0..9b861d5bde5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1319,9 +1319,11 @@ def disable_translations_once( @pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session") async def mock_zeroconf_resolver() -> AsyncGenerator[_patch]: """Mock out the zeroconf resolver.""" + resolver = AsyncResolver() + resolver.real_close = resolver.close patcher = patch( "homeassistant.helpers.aiohttp_client._async_make_resolver", - return_value=AsyncResolver(), + return_value=resolver, ) patcher.start() try: diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 6d2a7e7a8bb..e44111634d1 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -401,3 +401,15 @@ async def test_async_mdnsresolver( resp = await session.post("http://localhost/xyz", json={"x": 1}) assert resp.status == 200 assert await resp.json() == {"x": 1} + + +async def test_resolver_is_singleton(hass: HomeAssistant) -> None: + """Test that the resolver is a singleton.""" + session = client.async_get_clientsession(hass) + session2 = client.async_get_clientsession(hass) + session3 = client.async_create_clientsession(hass) + assert isinstance(session._connector, aiohttp.TCPConnector) + assert isinstance(session2._connector, aiohttp.TCPConnector) + assert isinstance(session3._connector, aiohttp.TCPConnector) + assert session._connector._resolver is session2._connector._resolver + assert session._connector._resolver is session3._connector._resolver From fe8e7b73bf5509a7bf09a7189aecf66f15a1a9ba Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 3 May 2025 11:51:26 +0200 Subject: [PATCH 025/110] Fix small issues with mqtt translations and improve readability (#144091) --- homeassistant/components/mqtt/strings.json | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index d2234121803..7339f3869a1 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -256,8 +256,8 @@ "green_template": "Green template", "last_reset_value_template": "Last reset value template", "optimistic": "Optimistic", - "payload_off": "Payload off", - "payload_on": "Payload on", + "payload_off": "Payload \"off\"", + "payload_on": "Payload \"on\"", "qos": "QoS", "red_template": "Red template", "retain": "Retain", @@ -278,7 +278,7 @@ "green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", - "on_command_type": "Defines when the `payload on` is sent. Using `last` (the default) will send any style (brightness, color, etc) topics first and then a `payload on` to the command_topic. Using `first` will send the `payload on` and then any style topics. Using `brightness` will only send brightness commands instead of the `Payload on` to turn the light on.", + "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", "payload_off": "The payload that represents the off state.", "payload_on": "The payload that represents the on state.", @@ -287,7 +287,7 @@ "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", "state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.", "state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)", - "supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, WHITE. Note that if onoff or brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", + "supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", "value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)" }, "sections": { @@ -325,7 +325,7 @@ "data_description": { "brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.", "brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.", - "brightness_command_topic": "The publishing topic that will be used to control the brigthness. [Learn more.]({url}#brightness_command_topic)", + "brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)", "brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.", "brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)", "brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value." @@ -385,7 +385,7 @@ "hs_value_template": "HS value template" }, "data_description": { - "hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to hs_command_topic. Available variables: `hue` and `sat`.", + "hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.", "hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)", "hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)", "hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value." @@ -574,15 +574,15 @@ "discovery": "Option to enable MQTT automatic discovery.", "discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.", "birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.", - "birth_topic": "The MQTT topic where Home Assistant will publish a `birth` message.", - "birth_payload": "The `birth` message that is published when MQTT is ready and connected.", - "birth_qos": "The quality of service of the `birth` message that is published when MQTT is ready and connected", - "birth_retain": "When set, Home Assistant will retain the `birth` message published to your MQTT broker.", - "will_enable": "When set, Home Assistant will ask your broker to publish a `will` message when MQTT is stopped or when it loses the connection to your broker.", - "will_topic": "The MQTT topic your MQTT broker will publish a `will` message to.", - "will_payload": "The message your MQTT broker `will` publish when the MQTT integration is stopped or when the connection is lost.", - "will_qos": "The quality of service of the `will` message that is published by your MQTT broker.", - "will_retain": "When set, your MQTT broker will retain the `will` message." + "birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.", + "birth_payload": "The \"birth\" message that is published when MQTT is ready and connected.", + "birth_qos": "The quality of service of the \"birth\" message that is published when MQTT is ready and connected", + "birth_retain": "When set, Home Assistant will retain the \"birth\" message published to your MQTT broker.", + "will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.", + "will_topic": "The MQTT topic your MQTT broker will publish a \"will\" message to.", + "will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.", + "will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.", + "will_retain": "When set, your MQTT broker will retain the \"will\" message." } } }, From 3ea3d77f4d49a9b3c9bf5703fd0077f21910e975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Bed=C5=99ich?= Date: Fri, 2 May 2025 13:49:33 +0200 Subject: [PATCH 026/110] Disable S3 checksums (#144092) Disable S3 checksums (#143995) --- homeassistant/components/s3/__init__.py | 7 +++++++ tests/components/s3/test_init.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/components/s3/__init__.py b/homeassistant/components/s3/__init__.py index 95e5e7d738c..ea6b8e244b1 100644 --- a/homeassistant/components/s3/__init__.py +++ b/homeassistant/components/s3/__init__.py @@ -7,6 +7,7 @@ from typing import cast from aiobotocore.client import AioBaseClient as S3Client from aiobotocore.session import AioSession +from botocore.config import Config from botocore.exceptions import ClientError, ConnectionError, ParamValidationError from homeassistant.config_entries import ConfigEntry @@ -32,6 +33,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: """Set up S3 from a config entry.""" data = cast(dict, entry.data) + # due to https://github.com/home-assistant/core/issues/143995 + config = Config( + request_checksum_calculation="when_required", + response_checksum_validation="when_required", + ) try: session = AioSession() # pylint: disable-next=unnecessary-dunder-call @@ -40,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: endpoint_url=data.get(CONF_ENDPOINT_URL), aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY], aws_access_key_id=data[CONF_ACCESS_KEY_ID], + config=config, ).__aenter__() await client.head_bucket(Bucket=data[CONF_BUCKET]) except ClientError as err: diff --git a/tests/components/s3/test_init.py b/tests/components/s3/test_init.py index afa11f5cf72..8255bbd0c66 100644 --- a/tests/components/s3/test_init.py +++ b/tests/components/s3/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +from botocore.config import Config from botocore.exceptions import ( ClientError, EndpointConnectionError, @@ -73,3 +74,19 @@ async def test_setup_entry_head_bucket_error( ) await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_checksum_settings_present( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that checksum validation is set to be compatible with third-party S3 providers.""" + # due to https://github.com/home-assistant/core/issues/143995 + with patch( + "homeassistant.components.s3.AioSession.create_client" + ) as mock_create_client: + await setup_integration(hass, mock_config_entry) + + config_arg = mock_create_client.call_args[1]["config"] + assert isinstance(config_arg, Config) + assert config_arg.request_checksum_calculation == "when_required" + assert config_arg.response_checksum_validation == "when_required" From 2e336626acb9a0e7d8aa09cb9c834dcf6fa66640 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Fri, 2 May 2025 11:34:58 -0400 Subject: [PATCH 027/110] bump aiokem to 0.5.9 (#144098) fix: bump aiokem to 0.5.9 --- homeassistant/components/rehlko/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rehlko/manifest.json b/homeassistant/components/rehlko/manifest.json index 93e284167f5..0c9f0c20e6f 100644 --- a/homeassistant/components/rehlko/manifest.json +++ b/homeassistant/components/rehlko/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_polling", "loggers": ["aiokem"], "quality_scale": "silver", - "requirements": ["aiokem==0.5.6"] + "requirements": ["aiokem==0.5.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index d431b80fba8..2beb7ab9632 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiokafka==0.10.0 aiokef==0.2.16 # homeassistant.components.rehlko -aiokem==0.5.6 +aiokem==0.5.9 # homeassistant.components.lifx aiolifx-effects==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28a3a286270..c6b808f4818 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aioimaplib==2.0.1 aiokafka==0.10.0 # homeassistant.components.rehlko -aiokem==0.5.6 +aiokem==0.5.9 # homeassistant.components.lifx aiolifx-effects==0.3.2 From 901926e8e6666136fcab5a2fc9841ae6315ffec7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 May 2025 23:33:39 +0300 Subject: [PATCH 028/110] Update frontend to 20250502.0 (#144114) --- 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 28b01aff616..2cfa9572ff3 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==20250430.2"] + "requirements": ["home-assistant-frontend==20250502.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c484a526374..6bcd21f4d99 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.45.0 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250430.2 +home-assistant-frontend==20250502.0 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2beb7ab9632..8f452b7d29f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250430.2 +home-assistant-frontend==20250502.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6b808f4818..8b317d0369e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250430.2 +home-assistant-frontend==20250502.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From aded44ee0f5dec5cfea31bfa8c65d1dd4add8a0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 15:53:19 -0500 Subject: [PATCH 029/110] Bump aiodns to 3.3.0 (#144115) --- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dnsip/test_config_flow.py | 24 ++++++++++++-------- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index d25459b95b7..35802adb7f3 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dnsip", "iot_class": "cloud_polling", - "requirements": ["aiodns==3.2.0"] + "requirements": ["aiodns==3.3.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bcd21f4d99..de493201acd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 -aiodns==3.2.0 +aiodns==3.3.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index d483ba2a636..a3dcb980470 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.13.2" dependencies = [ - "aiodns==3.2.0", + "aiodns==3.3.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 diff --git a/requirements.txt b/requirements.txt index 1e91dca8391..5bbf33025c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiodns==3.2.0 +aiodns==3.3.0 aiohasupervisor==0.3.1 aiohttp==3.11.18 aiohttp_cors==0.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8f452b7d29f..86052b41714 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.2.0 +aiodns==3.3.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b317d0369e..3df1199a4a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.2.0 +aiodns==3.3.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 9d92cb3554c..1a565345275 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -224,16 +224,20 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_RESOLVER: "8.8.8.8", - CONF_RESOLVER_IPV6: "2001:4860:4860::8888", - CONF_PORT: 53, - CONF_PORT_IPV6: 53, - }, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RESOLVER: "8.8.8.8", + CONF_RESOLVER_IPV6: "2001:4860:4860::8888", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, + }, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { From db0cf9fbf4a201f22215cdebe1b1a3febbef7921 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 2 May 2025 22:35:34 +0200 Subject: [PATCH 030/110] Bump aioautomower to 2025.5.1 (#144118) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 8e4be4c71f3..705975bb966 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["aioautomower"], "quality_scale": "silver", - "requirements": ["aioautomower==2025.4.4"] + "requirements": ["aioautomower==2025.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 86052b41714..3b93f9f40d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -201,7 +201,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2025.4.4 +aioautomower==2025.5.1 # homeassistant.components.azure_devops aioazuredevops==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3df1199a4a0..40997e5e24b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2025.4.4 +aioautomower==2025.5.1 # homeassistant.components.azure_devops aioazuredevops==2.2.1 From 7eee5ecd9a1df19e34663aa525bb75bcf5e1d2c3 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 2 May 2025 22:29:54 +0200 Subject: [PATCH 031/110] Fix intermittent unavailability for lamarzocco brew active sensor (#144120) * Fix brew active intermittent unavailability for lamarzocco * Whitespaces --- .../components/lamarzocco/binary_sensor.py | 2 +- .../components/lamarzocco/coordinator.py | 26 ++++++++++++++----- homeassistant/components/lamarzocco/entity.py | 5 ++-- homeassistant/components/lamarzocco/number.py | 14 +++++----- .../lamarzocco/test_binary_sensor.py | 11 ++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 98cf7cf222e..9bf04129095 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -52,7 +52,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( ).status is MachineState.BREWING ), - available_fn=lambda device: device.websocket.connected, + available_fn=lambda coordinator: not coordinator.websocket_terminated, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoBinarySensorEntityDescription( diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index 751ef550516..f0f64e02c28 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -44,6 +44,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): _default_update_interval = SCAN_INTERVAL config_entry: LaMarzoccoConfigEntry + websocket_terminated = True def __init__( self, @@ -92,15 +93,9 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): await self.device.get_dashboard() _LOGGER.debug("Current status: %s", self.device.dashboard.to_dict()) - _LOGGER.debug("Init WebSocket in background task") - self.config_entry.async_create_background_task( hass=self.hass, - target=self.device.connect_dashboard_websocket( - update_callback=lambda _: self.async_set_updated_data(None), - connect_callback=self.async_update_listeners, - disconnect_callback=self.async_update_listeners, - ), + target=self.connect_websocket(), name="lm_websocket_task", ) @@ -112,6 +107,23 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): ) self.config_entry.async_on_unload(websocket_close) + async def connect_websocket(self) -> None: + """Connect to the websocket.""" + + _LOGGER.debug("Init WebSocket in background task") + + self.websocket_terminated = False + self.async_update_listeners() + + await self.device.connect_dashboard_websocket( + update_callback=lambda _: self.async_set_updated_data(None), + connect_callback=self.async_update_listeners, + disconnect_callback=self.async_update_listeners, + ) + + self.websocket_terminated = True + self.async_update_listeners() + class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator): """Coordinator for La Marzocco settings.""" diff --git a/homeassistant/components/lamarzocco/entity.py b/homeassistant/components/lamarzocco/entity.py index 2e3a7f2ce83..6dc024645ce 100644 --- a/homeassistant/components/lamarzocco/entity.py +++ b/homeassistant/components/lamarzocco/entity.py @@ -3,7 +3,6 @@ from collections.abc import Callable from dataclasses import dataclass -from pylamarzocco import LaMarzoccoMachine from pylamarzocco.const import FirmwareType from homeassistant.const import CONF_ADDRESS, CONF_MAC @@ -23,7 +22,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator class LaMarzoccoEntityDescription(EntityDescription): """Description for all LM entities.""" - available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True + available_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True @@ -74,7 +73,7 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity): def available(self) -> bool: """Return True if entity is available.""" if super().available: - return self.entity_description.available_fn(self.coordinator.device) + return self.entity_description.available_fn(self.coordinator) return False def __init__( diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 81a03b4d6ee..7c4fe33a041 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -100,8 +100,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( .seconds.seconds_out ), available_fn=( - lambda machine: cast( - PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + lambda coordinator: cast( + PreBrewing, + coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING], ).mode is PreExtractionMode.PREINFUSION ), @@ -140,8 +141,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( .times.pre_brewing[0] .seconds.seconds_in ), - available_fn=lambda machine: cast( - PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + available_fn=lambda coordinator: cast( + PreBrewing, coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING] ).mode is PreExtractionMode.PREBREWING, supported_fn=( @@ -180,8 +181,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( .seconds.seconds_out ), available_fn=( - lambda machine: cast( - PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + lambda coordinator: cast( + PreBrewing, + coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING], ).mode is PreExtractionMode.PREBREWING ), diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 8e92c9bbba9..570b5aef8ec 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests for La Marzocco binary sensors.""" +from collections.abc import Generator from datetime import timedelta from unittest.mock import MagicMock, patch @@ -33,6 +34,16 @@ async def test_binary_sensors( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.fixture(autouse=True) +def mock_websocket_terminated() -> Generator[bool]: + """Mock websocket terminated.""" + with patch( + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated", + new=False, + ) as mock_websocket_terminated: + yield mock_websocket_terminated + + async def test_brew_active_unavailable( hass: HomeAssistant, mock_lamarzocco: MagicMock, From c2575735ffbce9595fe8fd0e654e7f061c114098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Sat, 3 May 2025 00:16:49 +0200 Subject: [PATCH 032/110] Update pywmspro to 0.2.2 to make error handling more robust (#144124) --- homeassistant/components/wmspro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wmspro/manifest.json b/homeassistant/components/wmspro/manifest.json index dd65be3e7e7..d4eda3a90a6 100644 --- a/homeassistant/components/wmspro/manifest.json +++ b/homeassistant/components/wmspro/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/wmspro", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["pywmspro==0.2.1"] + "requirements": ["pywmspro==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b93f9f40d7..8c223ab9bc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2577,7 +2577,7 @@ pywilight==0.0.74 pywizlight==0.6.2 # homeassistant.components.wmspro -pywmspro==0.2.1 +pywmspro==0.2.2 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40997e5e24b..3bf20a45ada 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2099,7 +2099,7 @@ pywilight==0.0.74 pywizlight==0.6.2 # homeassistant.components.wmspro -pywmspro==0.2.1 +pywmspro==0.2.2 # homeassistant.components.ws66i pyws66i==1.1 From f6a94d0661f9ce256c60658b6b41fd7babafbafd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 17:46:59 -0500 Subject: [PATCH 033/110] Bump PyISY to 3.4.1 (#144127) --- homeassistant/components/isy994/helpers.py | 3 +-- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 3686a182fe9..587c0544d6c 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -401,8 +401,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None: for dtype, _, node_id in folder.children: if dtype != TAG_FOLDER: continue - entity_folder = folder[node_id] - + entity_folder: Programs = folder[node_id] actions = None status = entity_folder.get_by_name(KEY_STATUS) if not status or status.protocol != PROTO_PROGRAM: diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 5cd3bb73a89..bbfc7deb80d 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -24,7 +24,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyisy"], - "requirements": ["pyisy==3.4.0"], + "requirements": ["pyisy==3.4.1"], "ssdp": [ { "manufacturer": "Universal Devices Inc.", diff --git a/requirements_all.txt b/requirements_all.txt index 8c223ab9bc2..40509329dd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2054,7 +2054,7 @@ pyiskra==0.1.15 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.4.0 +pyisy==3.4.1 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf20a45ada..4c561092925 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ pyiskra==0.1.15 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.4.0 +pyisy==3.4.1 # homeassistant.components.ituran pyituran==0.1.4 From 71bb8ae5291c44cc5e372ff171535e0c33e1586c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 03:08:56 -0500 Subject: [PATCH 034/110] Bump bleak-esphome to 2.15.1 (#144129) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index b07e78316d8..1f619b2017c 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"] + "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e2e3cb34721..beaf68decd9 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -19,7 +19,7 @@ "requirements": [ "aioesphomeapi==30.1.0", "esphome-dashboard-api==1.3.0", - "bleak-esphome==2.14.0" + "bleak-esphome==2.15.1" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 40509329dd2..d9a8d90ade0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -607,7 +607,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.14.0 +bleak-esphome==2.15.1 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c561092925..4ea60a459d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -538,7 +538,7 @@ bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.14.0 +bleak-esphome==2.15.1 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 From 2a5f031ba5f650a2124a15bd56d2ebd05d92c0e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 03:09:28 -0500 Subject: [PATCH 035/110] Bump Bluetooth deps to improve auto recovery process (#144133) --- .../components/bluetooth/manifest.json | 4 +- homeassistant/package_constraints.txt | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/bluetooth/test_wrappers.py | 59 ------------------- 5 files changed, 8 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1ffee18d8fb..5e74f7b5561 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,9 +18,9 @@ "bleak==0.22.3", "bleak-retry-connector==3.9.0", "bluetooth-adapters==0.21.4", - "bluetooth-auto-recovery==1.4.5", + "bluetooth-auto-recovery==1.5.1", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", - "habluetooth==3.45.0" + "habluetooth==3.47.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index de493201acd..8b53ae13687 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bcrypt==4.2.0 bleak-retry-connector==3.9.0 bleak==0.22.3 bluetooth-adapters==0.21.4 -bluetooth-auto-recovery==1.4.5 +bluetooth-auto-recovery==1.5.1 bluetooth-data-tools==1.28.1 cached-ipaddress==0.10.0 certifi>=2021.5.30 @@ -34,7 +34,7 @@ dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 -habluetooth==3.45.0 +habluetooth==3.47.1 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d9a8d90ade0..d52a573d7bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,7 @@ bluemaestro-ble==0.4.0 bluetooth-adapters==0.21.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.5 +bluetooth-auto-recovery==1.5.1 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -1118,7 +1118,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.45.0 +habluetooth==3.47.1 # homeassistant.components.cloud hass-nabucasa==0.96.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ea60a459d1..20e4f5af2d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ bluemaestro-ble==0.4.0 bluetooth-adapters==0.21.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.5 +bluetooth-auto-recovery==1.5.1 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -960,7 +960,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.45.0 +habluetooth==3.47.1 # homeassistant.components.cloud hass-nabucasa==0.96.0 diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index c5908776882..bfe7445f614 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -316,65 +316,6 @@ async def test_release_slot_on_connect_exception( cancel_hci1() -@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") -async def test_we_switch_adapters_on_failure( - hass: HomeAssistant, - install_bleak_catcher, -) -> None: - """Ensure we try the next best adapter after a failure.""" - hci0_device_advs, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices( - hass - ) - ble_device = hci0_device_advs["00:00:00:00:00:01"][0] - client = bleak.BleakClient(ble_device) - - class FakeBleakClientFailsHCI0Only(BaseFakeBleakClient): - """Fake bleak client that fails to connect.""" - - async def connect(self, *args, **kwargs): - """Connect.""" - if "/hci0/" in self._device.details["path"]: - return False - return True - - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is False - - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is False - - # After two tries we should switch to hci1 - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is True - - # ..and we remember that hci1 works as long as the client doesn't change - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is True - - # If we replace the client, we should try hci0 again - client = bleak.BleakClient(ble_device) - - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is False - cancel_hci0() - cancel_hci1() - - @pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_passing_subclassed_str_as_address( hass: HomeAssistant, From 16f36912db6c04781ccb2942f649f31091c5c155 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 May 2025 08:06:31 -0400 Subject: [PATCH 036/110] Bump version to 2025.5.0b2 --- 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 620ad2a1be3..f693f47443a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index a3dcb980470..395f7aeadc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b1" +version = "2025.5.0b2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From e95ed12ba1a19555b8d356fe30ecd9ae9a9d0ca1 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi <54689374+florian-sabonchi@users.noreply.github.com> Date: Sat, 3 May 2025 20:25:27 +0200 Subject: [PATCH 037/110] Fix check for locked device in AVM Fritz!SmartHome (#141697) * feat: raise execption on hvac mode while device is locked * fix: test for setting hvac mode while device is locked. * feat: update translation * feat: add separate translations for HVAC and temperature * fix: test cases * fix: test cases for test_set_preset_mode_boost * rev: code review * rev: exception string * feat: updated error message and added helper function * Update homeassistant/components/fritzbox/strings.json Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * fix: translation key * remove check_active_or_lock_mode from async_set_preset_mode --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/fritzbox/climate.py | 27 +++-- .../components/fritzbox/strings.json | 8 +- tests/components/fritzbox/test_climate.py | 113 +++++++++++++++++- 3 files changed, 130 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 194bc5621b3..573877fa71b 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" + self.check_active_or_lock_mode() if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF: await self.async_set_hkr_state("off") elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None: @@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" - if self.data.holiday_active or self.data.summer_active: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="change_hvac_while_active_mode", - ) + self.check_active_or_lock_mode() if self.hvac_mode is hvac_mode: LOGGER.debug( "%s is already in requested hvac mode %s", self.name, hvac_mode @@ -204,11 +201,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - if self.data.holiday_active or self.data.summer_active: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="change_preset_while_active_mode", - ) + self.check_active_or_lock_mode() await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode]) @property @@ -230,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open return attrs + + def check_active_or_lock_mode(self) -> None: + """Check if in summer/vacation mode or lock enabled.""" + if self.data.holiday_active or self.data.summer_active: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="change_settings_while_active_mode", + ) + + if self.data.lock: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="change_settings_while_lock_enabled", + ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index bb7d2f0fdf1..38bc6dc9c39 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -88,11 +88,11 @@ "manual_switching_disabled": { "message": "Can't toggle switch while manual switching is disabled for the device." }, - "change_preset_while_active_mode": { - "message": "Can't change preset while holiday or summer mode is active on the device." + "change_settings_while_lock_enabled": { + "message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device" }, - "change_hvac_while_active_mode": { - "message": "Can't change HVAC mode while holiday or summer mode is active on the device." + "change_settings_while_active_mode": { + "message": "Can't change settings while holiday or summer mode is active on the device." } } } diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 5bf81ef0238..bdf9dba8b42 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -211,6 +211,8 @@ async def test_set_temperature( ) -> None: """Test setting temperature.""" device = FritzDeviceClimateMock() + device.lock = False + await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -288,6 +290,8 @@ async def test_set_hvac_mode( ) -> None: """Test setting hvac mode.""" device = FritzDeviceClimateMock() + + device.lock = False device.target_temperature = target_temperature if current_preset is PRESET_COMFORT: @@ -335,6 +339,8 @@ async def test_set_preset_mode_comfort( ) -> None: """Test setting preset mode.""" device = FritzDeviceClimateMock() + + device.lock = False device.comfort_temperature = comfort_temperature await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz @@ -366,6 +372,8 @@ async def test_set_preset_mode_eco( ) -> None: """Test setting preset mode.""" device = FritzDeviceClimateMock() + + device.lock = False device.eco_temperature = eco_temperature await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz @@ -387,6 +395,8 @@ async def test_set_preset_mode_boost( ) -> None: """Test setting preset mode.""" device = FritzDeviceClimateMock() + device.lock = False + await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -471,11 +481,106 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: assert state +@pytest.mark.parametrize( + "service_data", + [ + {ATTR_TEMPERATURE: 23}, + { + ATTR_HVAC_MODE: HVACMode.HEAT, + ATTR_TEMPERATURE: 25, + }, + ], +) +async def test_set_temperature_lock( + hass: HomeAssistant, + fritz: Mock, + service_data: dict, +) -> None: + """Test setting temperature while device is locked.""" + device = FritzDeviceClimateMock() + + device.lock = True + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, **service_data}, + True, + ) + + +@pytest.mark.parametrize( + ("service_data", "target_temperature", "current_preset", "expected_call_args"), + [ + # mode off always sets target temperature to 0 + ({ATTR_HVAC_MODE: HVACMode.OFF}, 22, PRESET_COMFORT, [call(0, True)]), + ({ATTR_HVAC_MODE: HVACMode.OFF}, 16, PRESET_ECO, [call(0, True)]), + ({ATTR_HVAC_MODE: HVACMode.OFF}, 16, None, [call(0, True)]), + # mode heat sets target temperature based on current scheduled preset, + # when not already in mode heat + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_COMFORT, [call(22, True)]), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_ECO, [call(16, True)]), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, None, [call(22, True)]), + # mode heat does not set target temperature, when already in mode heat + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_COMFORT, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_ECO, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, None, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_COMFORT, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_ECO, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, None, []), + ], +) +async def test_set_hvac_mode_lock( + hass: HomeAssistant, + fritz: Mock, + service_data: dict, + target_temperature: float, + current_preset: str, + expected_call_args: list[_Call], +) -> None: + """Test setting hvac mode while device is locked.""" + device = FritzDeviceClimateMock() + + device.lock = True + device.target_temperature = target_temperature + + if current_preset is PRESET_COMFORT: + device.nextchange_temperature = device.eco_temperature + elif current_preset is PRESET_ECO: + device.nextchange_temperature = device.comfort_temperature + else: + device.nextchange_endperiod = 0 + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, **service_data}, + True, + ) + + async def test_holidy_summer_mode( hass: HomeAssistant, freezer: FrozenDateTimeFactory, fritz: Mock ) -> None: """Test holiday and summer mode.""" device = FritzDeviceClimateMock() + device.lock = False + await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -510,7 +615,7 @@ async def test_holidy_summer_mode( with pytest.raises( HomeAssistantError, - match="Can't change HVAC mode while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", @@ -520,7 +625,7 @@ async def test_holidy_summer_mode( ) with pytest.raises( HomeAssistantError, - match="Can't change preset while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", @@ -546,7 +651,7 @@ async def test_holidy_summer_mode( with pytest.raises( HomeAssistantError, - match="Can't change HVAC mode while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", @@ -556,7 +661,7 @@ async def test_holidy_summer_mode( ) with pytest.raises( HomeAssistantError, - match="Can't change preset while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", From 7322be2006874030b6c067b5cf712001b5c6bfc9 Mon Sep 17 00:00:00 2001 From: Charlie Rusbridger Date: Sat, 3 May 2025 20:10:33 +0100 Subject: [PATCH 038/110] Use kodi posters, fall back to thumbnails if unavailable. (#144066) --- homeassistant/components/kodi/browse_media.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 60e99d98cb1..3873f385881 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -152,7 +152,10 @@ async def item_payload(item, get_thumbnail_url=None): _LOGGER.debug("Unknown media type received: %s", media_content_type) raise UnknownMediaType from err - thumbnail = item.get("thumbnail") + if "art" in item: + thumbnail = item["art"].get("poster", item.get("thumbnail")) + else: + thumbnail = item.get("thumbnail") if thumbnail is not None and get_thumbnail_url is not None: thumbnail = await get_thumbnail_url( media_content_type, media_content_id, thumbnail_url=thumbnail @@ -237,14 +240,16 @@ async def get_media_info(media_library, search_id, search_type): title = None media = None - properties = ["thumbnail"] + properties = ["thumbnail", "art"] if search_type == MediaType.ALBUM: if search_id: album = await media_library.get_album_details( album_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - album["albumdetails"].get("thumbnail") + album["albumdetails"]["art"].get( + "poster", album["albumdetails"].get("thumbnail") + ) ) title = album["albumdetails"]["label"] media = await media_library.get_songs( @@ -256,6 +261,7 @@ async def get_media_info(media_library, search_id, search_type): "album", "thumbnail", "track", + "art", ], ) media = media.get("songs") @@ -274,7 +280,9 @@ async def get_media_info(media_library, search_id, search_type): artist_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - artist["artistdetails"].get("thumbnail") + artist["artistdetails"]["art"].get( + "poster", artist["artistdetails"].get("thumbnail") + ) ) title = artist["artistdetails"]["label"] else: @@ -293,9 +301,10 @@ async def get_media_info(media_library, search_id, search_type): movie_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - movie["moviedetails"].get("thumbnail") + movie["moviedetails"]["art"].get( + "poster", movie["moviedetails"].get("thumbnail") + ) ) - title = movie["moviedetails"]["label"] else: media = await media_library.get_movies(properties) media = media.get("movies") @@ -305,14 +314,16 @@ async def get_media_info(media_library, search_id, search_type): if search_id: media = await media_library.get_seasons( tv_show_id=int(search_id), - properties=["thumbnail", "season", "tvshowid"], + properties=["thumbnail", "season", "tvshowid", "art"], ) media = media.get("seasons") tvshow = await media_library.get_tv_show_details( tv_show_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - tvshow["tvshowdetails"].get("thumbnail") + tvshow["tvshowdetails"]["art"].get( + "poster", tvshow["tvshowdetails"].get("thumbnail") + ) ) title = tvshow["tvshowdetails"]["label"] else: @@ -325,7 +336,7 @@ async def get_media_info(media_library, search_id, search_type): media = await media_library.get_episodes( tv_show_id=int(tv_show_id), season_id=int(season_id), - properties=["thumbnail", "tvshowid", "seasonid"], + properties=["thumbnail", "tvshowid", "seasonid", "art"], ) media = media.get("episodes") if media: @@ -333,7 +344,9 @@ async def get_media_info(media_library, search_id, search_type): season_id=int(media[0]["seasonid"]), properties=properties ) thumbnail = media_library.thumbnail_url( - season["seasondetails"].get("thumbnail") + season["seasondetails"]["art"].get( + "poster", season["seasondetails"].get("thumbnail") + ) ) title = season["seasondetails"]["label"] @@ -343,6 +356,7 @@ async def get_media_info(media_library, search_id, search_type): properties=["thumbnail", "channeltype", "channel", "broadcastnow"], ) media = media.get("channels") + title = "Channels" return thumbnail, title, media From c5604395456ed6899467b398e58c2d0a289a85f9 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 3 May 2025 12:12:01 -0700 Subject: [PATCH 039/110] Skip the update right after the migration in Opower (#144088) * Wait for the migration to finish in Opower * Don't call async_block_till_done since this can timeout and seems to meant for tests * Don't call async_block_till_done since this can timeout and seems to meant for tests --- .../components/opower/coordinator.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index adb32d914ee..dd0b2c87bb5 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -190,7 +190,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): return_sum = 0.0 last_stats_time = None else: - await self._async_maybe_migrate_statistics( + migrated = await self._async_maybe_migrate_statistics( account.utility_account_id, { cost_statistic_id: compensation_statistic_id, @@ -203,6 +203,13 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): return_statistic_id: return_metadata, }, ) + if migrated: + # Skip update to avoid working on old data since the migration is done + # asynchronously. Update the statistics in the next refresh in 12h. + _LOGGER.debug( + "Statistics migration completed. Skipping update for now" + ) + continue cost_reads = await self._async_get_cost_reads( account, self.api.utility.timezone(), @@ -326,7 +333,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): utility_account_id: str, migration_map: dict[str, str], metadata_map: dict[str, StatisticMetaData], - ) -> None: + ) -> bool: """Perform one-time statistics migration based on the provided map. Splits negative values from source IDs into target IDs. @@ -339,7 +346,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): """ if not migration_map: - return + return False need_migration_source_ids = set() for source_id, target_id in migration_map.items(): @@ -354,7 +361,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): if not last_target_stat: need_migration_source_ids.add(source_id) if not need_migration_source_ids: - return + return False _LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids) @@ -416,7 +423,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): if not need_migration_source_ids: _LOGGER.debug("No migration needed") - return + return False for stat_id, stats in processed_stats.items(): _LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id) @@ -442,6 +449,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): }, ) + return True + async def _async_get_cost_reads( self, account: Account, time_zone_str: str, start_time: float | None = None ) -> list[CostRead]: From 89916b38e9209e6dbf6d62c0a1942fa4471a0a21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 10:12:37 -0500 Subject: [PATCH 040/110] Add tests to ensure ESPHome entity_ids are preserved on upgrade (#144116) --- tests/components/esphome/test_entity.py | 152 ++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 71a9c16cee3..ee6e6b6785f 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -10,8 +10,11 @@ from aioesphomeapi import ( BinarySensorState, SensorInfo, SensorState, + build_unique_id, ) +import pytest +from homeassistant.components.esphome import DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_RESTORED, @@ -19,6 +22,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -513,3 +517,151 @@ async def test_entity_without_name_device_with_friendly_name( # Make sure we have set the name to `None` as otherwise # the friendly_name will be "The Best Mixer " assert state.attributes[ATTR_FRIENDLY_NAME] == "The Best Mixer" + + +@pytest.mark.usefixtures("hass_storage") +async def test_entity_id_preserved_on_upgrade( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity_id is preserved on upgrade.""" + entity_info = [ + BinarySensorInfo( + object_id="my", + key=1, + name="my", + unique_id="binary_sensor_my", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + assert ( + build_unique_id("11:22:33:44:55:AA", entity_info[0]) + == "11:22:33:44:55:AA-binary_sensor-my" + ) + + entry = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, + DOMAIN, + "11:22:33:44:55:AA-binary_sensor-my", + suggested_object_id="should_not_change", + ) + assert entry.entity_id == "binary_sensor.should_not_change" + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"friendly_name": "The Best Mixer", "name": "mixer"}, + ) + state = hass.states.get("binary_sensor.should_not_change") + assert state is not None + + +@pytest.mark.usefixtures("hass_storage") +async def test_entity_id_preserved_on_upgrade_old_format_entity_id( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity_id is preserved on upgrade from old format.""" + entity_info = [ + BinarySensorInfo( + object_id="my", + key=1, + name="my", + unique_id="binary_sensor_my", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + assert ( + build_unique_id("11:22:33:44:55:AA", entity_info[0]) + == "11:22:33:44:55:AA-binary_sensor-my" + ) + + entry = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, + DOMAIN, + "11:22:33:44:55:AA-binary_sensor-my", + suggested_object_id="my", + ) + assert entry.entity_id == "binary_sensor.my" + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"name": "mixer"}, + ) + state = hass.states.get("binary_sensor.my") + assert state is not None + + +async def test_entity_id_preserved_on_upgrade_when_in_storage( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], + mock_esphome_device: MockESPHomeDeviceType, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity_id is preserved on upgrade with user defined entity_id.""" + entity_info = [ + BinarySensorInfo( + object_id="my", + key=1, + name="my", + unique_id="binary_sensor_my", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"friendly_name": "The Best Mixer", "name": "mixer"}, + ) + state = hass.states.get("binary_sensor.mixer_my") + assert state is not None + # now rename the entity + ent_reg_entry = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, + DOMAIN, + "11:22:33:44:55:AA-binary_sensor-my", + ) + entity_registry.async_update_entity( + ent_reg_entry.entity_id, + new_entity_id="binary_sensor.user_named", + ) + await hass.config_entries.async_unload(device.entry.entry_id) + await hass.async_block_till_done() + entry = device.entry + entry_id = entry.entry_id + storage_key = f"esphome.{entry_id}" + assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 + binary_sensor_data: dict[str, Any] = hass_storage[storage_key]["data"][ + "binary_sensor" + ][0] + assert binary_sensor_data["name"] == "my" + assert binary_sensor_data["object_id"] == "my" + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + entry=entry, + device_info={"friendly_name": "The Best Mixer", "name": "mixer"}, + ) + state = hass.states.get("binary_sensor.user_named") + assert state is not None From 35a1429e2b86e8a06e47283ff02dad509dcfb9ba Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 3 May 2025 14:44:18 +0200 Subject: [PATCH 041/110] Switch to common clientsession for lamarzocco (#144137) --- homeassistant/components/lamarzocco/__init__.py | 4 ++-- homeassistant/components/lamarzocco/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index ad9fec28fb4..ff977438f38 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_USE_BLUETOOTH, DOMAIN from .coordinator import ( @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - assert entry.unique_id serial = entry.unique_id - client = async_create_clientsession(hass) + client = async_get_clientsession(hass) cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index e352e337d0b..8cb2e4dfc61 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -33,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **user_input, } - self._client = async_create_clientsession(self.hass) + self._client = async_get_clientsession(self.hass) cloud_client = LaMarzoccoCloudClient( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], From ee125cd9a4e7336f59a5271645c86f61d9ae2bd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 18:22:48 -0500 Subject: [PATCH 042/110] Bump habluetooth to 3.48.2 (#144157) --- 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 5e74f7b5561..f9377443296 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.1", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", - "habluetooth==3.47.1" + "habluetooth==3.48.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b53ae13687..60d0f4e81fd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 -habluetooth==3.47.1 +habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d52a573d7bd..adb4d14646d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1118,7 +1118,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.47.1 +habluetooth==3.48.2 # homeassistant.components.cloud hass-nabucasa==0.96.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20e4f5af2d1..f630e4ed6ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -960,7 +960,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.47.1 +habluetooth==3.48.2 # homeassistant.components.cloud hass-nabucasa==0.96.0 From fb9f8e3581cecad0885af47668d03b755ed8d3af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 14:21:03 -0500 Subject: [PATCH 043/110] Bump zeroconf to 0.147.0 (#144158) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e2637d792e2..fe190e78956 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.146.5"] + "requirements": ["zeroconf==0.147.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 60d0f4e81fd..73415df8abd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -75,7 +75,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 yarl==1.20.0 -zeroconf==0.146.5 +zeroconf==0.147.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/pyproject.toml b/pyproject.toml index 395f7aeadc8..1f84097f4a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ dependencies = [ "voluptuous-openapi==0.0.7", "yarl==1.20.0", "webrtc-models==0.3.0", - "zeroconf==0.146.5", + "zeroconf==0.147.0", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index 5bbf33025c3..e8b9e12bfe0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,4 @@ voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.7 yarl==1.20.0 webrtc-models==0.3.0 -zeroconf==0.146.5 +zeroconf==0.147.0 diff --git a/requirements_all.txt b/requirements_all.txt index adb4d14646d..be4709419ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3156,7 +3156,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.146.5 +zeroconf==0.147.0 # homeassistant.components.zeversolar zeversolar==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f630e4ed6ec..686aa81a4a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2555,7 +2555,7 @@ yt-dlp[default]==2025.03.31 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.146.5 +zeroconf==0.147.0 # homeassistant.components.zeversolar zeversolar==0.3.2 From 07a03ee10d25123492548f4a5bf506ce74e1ed10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 May 2025 17:21:22 -0400 Subject: [PATCH 044/110] Point thumbnail TTS media source to right logo (#144162) --- homeassistant/components/tts/media_source.py | 2 +- tests/components/tts/test_media_source.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index d3c0998bb77..f096e082364 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -180,7 +180,7 @@ class TTSMediaSource(MediaSource): raise BrowseError("Unknown provider") if isinstance(engine_instance, TextToSpeechEntity): - engine_domain = engine_instance.platform.domain + engine_domain = engine_instance.platform.platform_name else: engine_domain = engine diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index c9d70c7f43e..eb4b09cab5b 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -78,6 +78,7 @@ async def test_browsing(hass: HomeAssistant, setup: str) -> None: assert item_child.children is None assert item_child.can_play is False assert item_child.can_expand is True + assert item_child.thumbnail == "https://brands.home-assistant.io/_/test/logo.png" item_child = await media_source.async_browse_media( hass, item.children[0].media_content_id + "?message=bla" From 99e13278e3353d627dbeb4d48485c72e2c70ed78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 4 May 2025 11:30:37 +0200 Subject: [PATCH 045/110] Bump pymiele to 0.4.3 (#144176) * Use device class transation * Bump pymiele to 0.4.3 --------- Co-authored-by: Shay Levy --- homeassistant/components/miele/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index dc9b420e07e..c0795922875 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.4.1"], + "requirements": ["pymiele==0.4.3"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index be4709419ab..80467891b8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2135,7 +2135,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.4.1 +pymiele==0.4.3 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 686aa81a4a5..6650853d379 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1747,7 +1747,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.4.1 +pymiele==0.4.3 # homeassistant.components.mochad pymochad==0.2.0 From 5b12bdca00456151b3c703ed4bee6d5e769c05b8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 4 May 2025 10:02:11 +0200 Subject: [PATCH 046/110] Fix licenses check for setuptools (#144181) --- script/licenses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/licenses.py b/script/licenses.py index aed3bec9998..f801603738a 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -208,7 +208,6 @@ EXCEPTIONS = { # https://github.com/jaraco/skeleton/pull/170 # https://github.com/jaraco/skeleton/pull/171 "jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21 - "setuptools", # MIT } TODO = { From 63679333cc27d2b34ebf7a6e4d2f709602ca355f Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Sun, 4 May 2025 12:07:18 +0200 Subject: [PATCH 047/110] Bump homematicip to 2.0.1.1 (#144182) Co-authored-by: Shay Levy --- homeassistant/components/homematicip_cloud/hap.py | 2 +- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homematicip_cloud/test_hap.py | 2 +- tests/components/homematicip_cloud/test_init.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index d55b98b8c18..6f98836a1ff 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -9,10 +9,10 @@ from typing import Any from homematicip.async_home import AsyncHome from homematicip.auth import Auth -from homematicip.base.base_connection import HmipConnectionError from homematicip.base.enums import EventType from homematicip.connection.connection_context import ConnectionContextBuilder from homematicip.connection.rest_connection import RestConnection +from homematicip.exceptions.connection_exceptions import HmipConnectionError import homeassistant from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index afd5863891d..15bc24c110f 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", "loggers": ["homematicip"], - "requirements": ["homematicip==2.0.1"] + "requirements": ["homematicip==2.0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 80467891b8d..9d116efa284 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ home-assistant-frontend==20250502.0 home-assistant-intents==2025.4.30 # homeassistant.components.homematicip_cloud -homematicip==2.0.1 +homematicip==2.0.1.1 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6650853d379..b14067bfd17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -997,7 +997,7 @@ home-assistant-frontend==20250502.0 home-assistant-intents==2025.4.30 # homeassistant.components.homematicip_cloud -homematicip==2.0.1 +homematicip==2.0.1.1 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 1732459149c..e34424d3439 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -3,8 +3,8 @@ from unittest.mock import Mock, patch from homematicip.auth import Auth -from homematicip.base.base_connection import HmipConnectionError from homematicip.connection.connection_context import ConnectionContext +from homematicip.exceptions.connection_exceptions import HmipConnectionError import pytest from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index a3578baa9aa..f28b3870705 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import AsyncMock, Mock, patch -from homematicip.base.base_connection import HmipConnectionError from homematicip.connection.connection_context import ConnectionContext +from homematicip.exceptions.connection_exceptions import HmipConnectionError from homeassistant.components.homematicip_cloud.const import ( CONF_ACCESSPOINT, From 8ce0b6b4b30c94acf421b9ab9a17899733df22d7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 4 May 2025 12:08:07 +0200 Subject: [PATCH 048/110] Add missing pollen category to AccuWeather (#144185) * Add extreme level to pollen map * Sort * Sort --- homeassistant/components/accuweather/const.py | 1 + homeassistant/components/accuweather/strings.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 7216f5a0b9b..e1dc4a9abcb 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = { 2: "moderate", 3: "high", 4: "very_high", + 5: "extreme", } UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40) UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index e81ef782d98..19e52be1ce3 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -72,6 +72,7 @@ "level": { "name": "Level", "state": { + "extreme": "Extreme", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "Moderate", @@ -89,6 +90,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -123,6 +125,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -167,6 +170,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -181,6 +185,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -195,6 +200,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", From 7a7bd9c62193e0061629226b5bf1b15550323219 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 May 2025 12:00:40 -0400 Subject: [PATCH 049/110] Fix intent TurnOn creating stack trace for buttons (#144205) --- homeassistant/components/intent/__init__.py | 28 +++- tests/components/intent/test_init.py | 135 ++++++++++++++++---- 2 files changed, 140 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index dfbe8d0135c..72853276ab3 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -10,6 +10,11 @@ from aiohttp import web import voluptuous as vol from homeassistant.components import http, sensor +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + SERVICE_PRESS as SERVICE_PRESS_BUTTON, + ButtonDeviceClass, +) from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import ( ATTR_POSITION, @@ -20,6 +25,7 @@ from homeassistant.components.cover import ( CoverDeviceClass, ) from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, @@ -80,6 +86,7 @@ __all__ = [ ] ONOFF_DEVICE_CLASSES = { + ButtonDeviceClass, CoverDeviceClass, ValveDeviceClass, SwitchDeviceClass, @@ -103,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: intent.INTENT_TURN_ON, HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON, - description="Turns on/opens a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.", + description="Turns on/opens/presses a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.", device_classes=ONOFF_DEVICE_CLASSES, ), ) @@ -168,6 +175,25 @@ class OnOffIntentHandler(intent.ServiceIntentHandler): """Call service on entity with handling for special cases.""" hass = intent_obj.hass + if state.domain in (BUTTON_DOMAIN, INPUT_BUTTON_DOMAIN): + if service != SERVICE_TURN_ON: + raise intent.IntentHandleError( + f"Entity {state.entity_id} cannot be turned off" + ) + + await self._run_then_background( + hass.async_create_task( + hass.services.async_call( + state.domain, + SERVICE_PRESS_BUTTON, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + blocking=True, + ) + ) + ) + return + if state.domain == COVER_DOMAIN: # on = open # off = close diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 0db9682d0ad..3779930e360 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -2,8 +2,10 @@ import pytest -from homeassistant.components.cover import SERVICE_OPEN_COVER -from homeassistant.components.lock import SERVICE_LOCK +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.cover import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER +from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.components.valve import SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -121,41 +123,130 @@ async def test_turn_on_intent(hass: HomeAssistant) -> None: assert call.data == {"entity_id": ["light.test_light"]} -async def test_translated_turn_on_intent( +@pytest.mark.parametrize("domain", ["button", "input_button"]) +async def test_turn_on_intent_button( + hass: HomeAssistant, entity_registry: er.EntityRegistry, domain +) -> None: + """Test HassTurnOn intent on button domains.""" + assert await async_setup_component(hass, "intent", {}) + + button = entity_registry.async_get_or_create(domain, "test", "button_uid") + + hass.states.async_set(button.entity_id, "unknown") + button_service_calls = async_mock_service(hass, domain, SERVICE_PRESS) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": button.entity_id}} + ) + + await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": button.entity_id}} + ) + + assert len(button_service_calls) == 1 + call = button_service_calls[0] + assert call.domain == domain + assert call.service == SERVICE_PRESS + assert call.data == {"entity_id": button.entity_id} + + +async def test_turn_on_off_intent_valve( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: - """Test HassTurnOn intent on domains which don't have the intent.""" - result = await async_setup_component(hass, "homeassistant", {}) - result = await async_setup_component(hass, "intent", {}) - await hass.async_block_till_done() - assert result + """Test HassTurnOn/Off intent on valve domains.""" + assert await async_setup_component(hass, "intent", {}) + + valve = entity_registry.async_get_or_create("valve", "test", "valve_uid") + + hass.states.async_set(valve.entity_id, "closed") + open_calls = async_mock_service(hass, "valve", SERVICE_OPEN_VALVE) + close_calls = async_mock_service(hass, "valve", SERVICE_CLOSE_VALVE) + + await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": valve.entity_id}} + ) + + assert len(open_calls) == 1 + call = open_calls[0] + assert call.domain == "valve" + assert call.service == SERVICE_OPEN_VALVE + assert call.data == {"entity_id": valve.entity_id} + + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": valve.entity_id}} + ) + + assert len(close_calls) == 1 + call = close_calls[0] + assert call.domain == "valve" + assert call.service == SERVICE_CLOSE_VALVE + assert call.data == {"entity_id": valve.entity_id} + + +async def test_turn_on_off_intent_cover( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test HassTurnOn/Off intent on cover domains.""" + assert await async_setup_component(hass, "intent", {}) cover = entity_registry.async_get_or_create("cover", "test", "cover_uid") - lock = entity_registry.async_get_or_create("lock", "test", "lock_uid") hass.states.async_set(cover.entity_id, "closed") - hass.states.async_set(lock.entity_id, "unlocked") - cover_service_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - lock_service_calls = async_mock_service(hass, "lock", SERVICE_LOCK) + open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": cover.entity_id}} ) + + assert len(open_calls) == 1 + call = open_calls[0] + assert call.domain == "cover" + assert call.service == SERVICE_OPEN_COVER + assert call.data == {"entity_id": cover.entity_id} + + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": cover.entity_id}} + ) + + assert len(close_calls) == 1 + call = close_calls[0] + assert call.domain == "cover" + assert call.service == SERVICE_CLOSE_COVER + assert call.data == {"entity_id": cover.entity_id} + + +async def test_turn_on_off_intent_lock( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test HassTurnOn/Off intent on lock domains.""" + assert await async_setup_component(hass, "intent", {}) + + lock = entity_registry.async_get_or_create("lock", "test", "lock_uid") + + hass.states.async_set(lock.entity_id, "locked") + unlock_calls = async_mock_service(hass, "lock", SERVICE_UNLOCK) + lock_calls = async_mock_service(hass, "lock", SERVICE_LOCK) + await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": lock.entity_id}} ) - await hass.async_block_till_done() - assert len(cover_service_calls) == 1 - call = cover_service_calls[0] - assert call.domain == "cover" - assert call.service == "open_cover" - assert call.data == {"entity_id": cover.entity_id} - - assert len(lock_service_calls) == 1 - call = lock_service_calls[0] + assert len(lock_calls) == 1 + call = lock_calls[0] assert call.domain == "lock" - assert call.service == "lock" + assert call.service == SERVICE_LOCK + assert call.data == {"entity_id": lock.entity_id} + + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": lock.entity_id}} + ) + + assert len(unlock_calls) == 1 + call = unlock_calls[0] + assert call.domain == "lock" + assert call.service == SERVICE_UNLOCK assert call.data == {"entity_id": lock.entity_id} From 2d3259413acd43c8178d4b545dbb0bff3eedee00 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 May 2025 16:11:29 +0000 Subject: [PATCH 050/110] Bump version to 2025.5.0b3 --- 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 f693f47443a..86caae51ea9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 1f84097f4a4..600f8b0112c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b2" +version = "2025.5.0b3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From d51eda40b324598af482c1ac7363ea7caaee8c90 Mon Sep 17 00:00:00 2001 From: Luca De Petrillo <972242+lukakama@users.noreply.github.com> Date: Mon, 5 May 2025 14:30:36 +0200 Subject: [PATCH 051/110] Fix message corruption in picotts component (#141182) --- homeassistant/components/picotts/tts.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 44d33145b3d..54caf1a2b26 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -56,10 +56,15 @@ class PicoProvider(Provider): with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf: fname = tmpf.name - cmd = ["pico2wave", "--wave", fname, "-l", language, "--", message] - subprocess.call(cmd) + cmd = ["pico2wave", "--wave", fname, "-l", language] + result = subprocess.run(cmd, text=True, input=message, check=False) data = None try: + if result.returncode != 0: + _LOGGER.error( + "Error running pico2wave, return code: %s", result.returncode + ) + return (None, None) with open(fname, "rb") as voice: data = voice.read() except OSError: From 1d0c520f6499b6ef340bdce3d39d496e280cc05b Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 5 May 2025 05:36:58 -0700 Subject: [PATCH 052/110] Use names instead of statistic IDs in the Opower repair issue (#144018) * Use names instead of statistic IDs in the Opower repair issue * target_ids --- homeassistant/components/opower/coordinator.py | 2 +- homeassistant/components/opower/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index dd0b2c87bb5..ff0e3264b48 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -441,7 +441,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): "energy_settings": "/config/energy", "target_ids": "\n".join( { - v + str(metadata_map[v]["name"]) for k, v in migration_map.items() if k in need_migration_source_ids } diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index b0516f266a1..f65aeb011ee 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -35,7 +35,7 @@ "issues": { "return_to_grid_migration": { "title": "Return to grid statistics for account: {utility_account_id}", - "description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}" + "description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue." } } } From 34bec1c50fdecdcb0edef950ad7723add04fe490 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 4 May 2025 12:41:39 -0400 Subject: [PATCH 053/110] Avoid delaying HA startup in Rehlko (#144202) --- homeassistant/components/rehlko/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index 19702527259..49ceb8ac870 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -40,7 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo ) rehlko.set_refresh_token_callback(async_refresh_token_update) - rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20]) try: await rehlko.authenticate( @@ -48,6 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo entry.data[CONF_PASSWORD], entry.data.get(CONF_REFRESH_TOKEN), ) + homes = await rehlko.get_homes() except AuthenticationError as ex: raise ConfigEntryAuthFailed( translation_domain=DOMAIN, @@ -60,7 +60,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo translation_key="cannot_connect", ) from ex coordinators: dict[int, RehlkoUpdateCoordinator] = {} - homes = await rehlko.get_homes() entry.runtime_data = RehlkoRuntimeData( coordinators=coordinators, @@ -86,6 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo await coordinator.async_config_entry_first_refresh() coordinators[device_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Retrys enabled after successful connection to prevent blocking startup + rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20]) return True From 00a14a0824af5e7c4340b926fa5711b9eb0c6610 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 4 May 2025 12:15:01 -0400 Subject: [PATCH 054/110] bump aiokem to 0.5.10 (#144203) --- homeassistant/components/rehlko/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rehlko/manifest.json b/homeassistant/components/rehlko/manifest.json index 0c9f0c20e6f..6b2f6190883 100644 --- a/homeassistant/components/rehlko/manifest.json +++ b/homeassistant/components/rehlko/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_polling", "loggers": ["aiokem"], "quality_scale": "silver", - "requirements": ["aiokem==0.5.9"] + "requirements": ["aiokem==0.5.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d116efa284..62cd159b4b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiokafka==0.10.0 aiokef==0.2.16 # homeassistant.components.rehlko -aiokem==0.5.9 +aiokem==0.5.10 # homeassistant.components.lifx aiolifx-effects==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b14067bfd17..ea6c7a12e25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aioimaplib==2.0.1 aiokafka==0.10.0 # homeassistant.components.rehlko -aiokem==0.5.9 +aiokem==0.5.10 # homeassistant.components.lifx aiolifx-effects==0.3.2 From 8424f179e498a8f4175fe61eb5e58b6744b86fe6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 5 May 2025 04:13:08 -0700 Subject: [PATCH 055/110] Fix Office 365 calendars to be compatible with rfc5545 (#144230) --- .../components/remote_calendar/config_flow.py | 13 +---- .../components/remote_calendar/coordinator.py | 12 +--- .../components/remote_calendar/ics.py | 44 ++++++++++++++ .../snapshots/test_calendar.ambr | 19 ++++++ .../remote_calendar/test_calendar.py | 30 ++++++++++ .../testdata/office365_invalid_tzid.ics | 58 +++++++++++++++++++ 6 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/remote_calendar/ics.py create mode 100644 tests/components/remote_calendar/snapshots/test_calendar.ambr create mode 100644 tests/components/remote_calendar/testdata/office365_invalid_tzid.ics diff --git a/homeassistant/components/remote_calendar/config_flow.py b/homeassistant/components/remote_calendar/config_flow.py index 802a7eb7cea..558a3d668ae 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -5,8 +5,6 @@ import logging from typing import Any from httpx import HTTPError, InvalidURL -from ical.calendar_stream import IcsCalendarStream -from ical.exceptions import CalendarParseError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -14,6 +12,7 @@ from homeassistant.const import CONF_URL from homeassistant.helpers.httpx_client import get_async_client from .const import CONF_CALENDAR_NAME, DOMAIN +from .ics import InvalidIcsException, parse_calendar _LOGGER = logging.getLogger(__name__) @@ -64,15 +63,9 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.debug("An error occurred: %s", err) else: try: - await self.hass.async_add_executor_job( - IcsCalendarStream.calendar_from_ics, res.text - ) - except CalendarParseError as err: + await parse_calendar(self.hass, res.text) + except InvalidIcsException: errors["base"] = "invalid_ics_file" - _LOGGER.error("Error reading the calendar information: %s", err.message) - _LOGGER.debug( - "Additional calendar error detail: %s", str(err.detailed_error) - ) else: return self.async_create_entry( title=user_input[CONF_CALENDAR_NAME], data=user_input diff --git a/homeassistant/components/remote_calendar/coordinator.py b/homeassistant/components/remote_calendar/coordinator.py index 6caec297c1a..1eead7682d3 100644 --- a/homeassistant/components/remote_calendar/coordinator.py +++ b/homeassistant/components/remote_calendar/coordinator.py @@ -5,8 +5,6 @@ import logging from httpx import HTTPError, InvalidURL from ical.calendar import Calendar -from ical.calendar_stream import IcsCalendarStream -from ical.exceptions import CalendarParseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL @@ -15,6 +13,7 @@ from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN +from .ics import InvalidIcsException, parse_calendar type RemoteCalendarConfigEntry = ConfigEntry[RemoteCalendarDataUpdateCoordinator] @@ -56,14 +55,9 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]): translation_placeholders={"err": str(err)}, ) from err try: - # calendar_from_ics will dynamically load packages - # the first time it is called, so we need to do it - # in a separate thread to avoid blocking the event loop self.ics = res.text - return await self.hass.async_add_executor_job( - IcsCalendarStream.calendar_from_ics, self.ics - ) - except CalendarParseError as err: + return await parse_calendar(self.hass, res.text) + except InvalidIcsException as err: raise UpdateFailed( translation_domain=DOMAIN, translation_key="unable_to_parse", diff --git a/homeassistant/components/remote_calendar/ics.py b/homeassistant/components/remote_calendar/ics.py new file mode 100644 index 00000000000..d0920d7ae32 --- /dev/null +++ b/homeassistant/components/remote_calendar/ics.py @@ -0,0 +1,44 @@ +"""Module for parsing ICS content. + +This module exists to fix known issues where calendar providers return calendars +that do not follow rfcc5545. This module will attempt to fix the calendar and return +a valid calendar object. +""" + +import logging + +from ical.calendar import Calendar +from ical.calendar_stream import IcsCalendarStream +from ical.compat import enable_compat_mode +from ical.exceptions import CalendarParseError + +from homeassistant.core import HomeAssistant + +_LOGGER = logging.getLogger(__name__) + + +class InvalidIcsException(Exception): + """Exception to indicate that the ICS content is invalid.""" + + +def _compat_calendar_from_ics(ics: str) -> Calendar: + """Parse the ICS content and return a Calendar object. + + This function is called in a separate thread to avoid blocking the event + loop while loading packages or parsing the ICS content for large calendars. + + It uses the `enable_compat_mode` context manager to fix known issues with + calendar providers that return invalid calendars. + """ + with enable_compat_mode(ics) as compat_ics: + return IcsCalendarStream.calendar_from_ics(compat_ics) + + +async def parse_calendar(hass: HomeAssistant, ics: str) -> Calendar: + """Parse the ICS content and return a Calendar object.""" + try: + return await hass.async_add_executor_job(_compat_calendar_from_ics, ics) + except CalendarParseError as err: + _LOGGER.error("Error parsing calendar information: %s", err.message) + _LOGGER.debug("Additional calendar error detail: %s", str(err.detailed_error)) + raise InvalidIcsException(err.message) from err diff --git a/tests/components/remote_calendar/snapshots/test_calendar.ambr b/tests/components/remote_calendar/snapshots/test_calendar.ambr new file mode 100644 index 00000000000..e372be5255c --- /dev/null +++ b/tests/components/remote_calendar/snapshots/test_calendar.ambr @@ -0,0 +1,19 @@ +# serializer version: 1 +# name: test_calendar_examples[office365_invalid_tzid] + list([ + dict({ + 'description': None, + 'end': dict({ + 'dateTime': '2024-04-26T15:00:00-06:00', + }), + 'location': '', + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'dateTime': '2024-04-26T14:00:00-06:00', + }), + 'summary': 'Uffe', + 'uid': '040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000010000000309AE93C8C3A94489F90ADBEA30C2F2B', + }), + ]) +# --- diff --git a/tests/components/remote_calendar/test_calendar.py b/tests/components/remote_calendar/test_calendar.py index 6ae817321c3..a0c18383369 100644 --- a/tests/components/remote_calendar/test_calendar.py +++ b/tests/components/remote_calendar/test_calendar.py @@ -1,11 +1,13 @@ """Tests for calendar platform of Remote Calendar.""" from datetime import datetime +import pathlib import textwrap from httpx import Response import pytest import respx +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -21,6 +23,13 @@ from .conftest import ( from tests.common import MockConfigEntry +# Test data files with known calendars from various sources. You can add a new file +# in the testdata directory and add it will be parsed and tested. +TESTDATA_FILES = sorted( + pathlib.Path("tests/components/remote_calendar/testdata/").glob("*.ics") +) +TESTDATA_IDS = [f.stem for f in TESTDATA_FILES] + @respx.mock async def test_empty_calendar( @@ -392,3 +401,24 @@ async def test_all_day_iter_order( events = await get_events("2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z") assert [event["summary"] for event in events] == event_order + + +@respx.mock +@pytest.mark.parametrize("ics_filename", TESTDATA_FILES, ids=TESTDATA_IDS) +async def test_calendar_examples( + hass: HomeAssistant, + config_entry: MockConfigEntry, + get_events: GetEventsFn, + ics_filename: pathlib.Path, + snapshot: SnapshotAssertion, +) -> None: + """Test parsing known calendars form test data files.""" + respx.get(CALENDER_URL).mock( + return_value=Response( + status_code=200, + text=ics_filename.read_text(), + ) + ) + await setup_integration(hass, config_entry) + events = await get_events("1997-07-14T00:00:00", "2025-07-01T00:00:00") + assert events == snapshot diff --git a/tests/components/remote_calendar/testdata/office365_invalid_tzid.ics b/tests/components/remote_calendar/testdata/office365_invalid_tzid.ics new file mode 100644 index 00000000000..bfadba446d2 --- /dev/null +++ b/tests/components/remote_calendar/testdata/office365_invalid_tzid.ics @@ -0,0 +1,58 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Kalender +BEGIN:VTIMEZONE +TZID:W. Europe Standard Time +BEGIN:STANDARD +DTSTART:16010101T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:UTC +BEGIN:STANDARD +DTSTART:16010101T000000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T000000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000 + 010000000309AE93C8C3A94489F90ADBEA30C2F2B +SUMMARY:Uffe +DTSTART;TZID=Customized Time Zone:20240426T140000 +DTEND;TZID=Customized Time Zone:20240426T150000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20250417T155647Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +END:VCALENDAR From 1c260cfb00d14d0f7155dce70714e6ec87a213b3 Mon Sep 17 00:00:00 2001 From: Eliz Date: Mon, 5 May 2025 18:11:41 +0100 Subject: [PATCH 056/110] Fix missing head forwarding in ingress (#144231) * Add support for connect, head and trace in ingress * added tests * update the testutil * fix * fix empty space * removed connect * remove trace --- homeassistant/components/hassio/ingress.py | 1 + tests/components/hassio/test_ingress.py | 43 ++++++++++++++++++++++ tests/test_util/aiohttp.py | 4 ++ 3 files changed, 48 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index a2f5a43b69c..e673c3a70e9 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -109,6 +109,7 @@ class HassIOIngress(HomeAssistantView): delete = _handle patch = _handle options = _handle + head = _handle async def _handle_websocket( self, request: web.Request, token: str, path: str diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 805b5292edb..069abaa8513 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -269,6 +269,49 @@ async def test_ingress_request_options( assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] +@pytest.mark.parametrize( + "build_type", + [ + ("a3_vl", "test/beer/ping?index=1"), + ("core", "index.html"), + ("local", "panel/config"), + ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", ""), + ], +) +async def test_ingress_request_head( + hassio_noauth_client, build_type, aioclient_mock: AiohttpClientMocker +) -> None: + """Test no auth needed for .""" + aioclient_mock.head( + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", + text="test", + ) + + resp = await hassio_noauth_client.head( + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", + headers={"X-Test-Header": "beer"}, + ) + + # Check we got right response + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "" # head does not return a body + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert X_AUTH_TOKEN not in aioclient_mock.mock_calls[-1][3] + assert aioclient_mock.mock_calls[-1][3]["X-Hass-Source"] == "core.ingress" + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + @pytest.mark.parametrize( "build_type", [ diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 633f98dc5b3..9207ba0904b 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -110,6 +110,10 @@ class AiohttpClientMocker: """Register a mock patch request.""" self.request("patch", *args, **kwargs) + def head(self, *args, **kwargs): + """Register a mock head request.""" + self.request("head", *args, **kwargs) + @property def call_count(self): """Return the number of requests made.""" From 7976e1b104984476d086d729058f6da5d1971c6e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:07:02 -0700 Subject: [PATCH 057/110] Update remote calendar to do all event handling in an executor (#144232) --- .../components/remote_calendar/calendar.py | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/remote_calendar/calendar.py b/homeassistant/components/remote_calendar/calendar.py index bd83a5f18cc..2f60918f010 100644 --- a/homeassistant/components/remote_calendar/calendar.py +++ b/homeassistant/components/remote_calendar/calendar.py @@ -29,7 +29,7 @@ async def async_setup_entry( """Set up the remote calendar platform.""" coordinator = entry.runtime_data entity = RemoteCalendarEntity(coordinator, entry) - async_add_entities([entity]) + async_add_entities([entity], True) class RemoteCalendarEntity( @@ -48,25 +48,46 @@ class RemoteCalendarEntity( super().__init__(coordinator) self._attr_name = entry.data[CONF_CALENDAR_NAME] self._attr_unique_id = entry.entry_id + self._event: CalendarEvent | None = None @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - now = dt_util.now() - events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now) - if event := next(events, None): - return _get_calendar_event(event) - return None + return self._event async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping( - start_date, - end_date, - ) - return [_get_calendar_event(event) for event in events] + + def events_in_range() -> list[CalendarEvent]: + """Return all events in the given time range.""" + events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping( + start_date, + end_date, + ) + return [_get_calendar_event(event) for event in events] + + return await self.hass.async_add_executor_job(events_in_range) + + async def async_update(self) -> None: + """Refresh the timeline. + + This is called when the coordinator updates. Creating the timeline may + require walking through the entire calendar and handling recurring + events, so it is done as a separate task without blocking the event loop. + """ + await super().async_update() + + def next_timeline_event() -> CalendarEvent | None: + """Return the next active event.""" + now = dt_util.now() + events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now) + if event := next(events, None): + return _get_calendar_event(event) + return None + + self._event = await self.hass.async_add_executor_job(next_timeline_event) def _get_calendar_event(event: Event) -> CalendarEvent: From 6f77d0b0d52cfb03748b051b7deee8aa2c6d1305 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:06:27 -0700 Subject: [PATCH 058/110] Update local calendar to process calendar events in the executor (#144233) --- .../components/local_calendar/calendar.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index df6f994a46c..639cf5234d1 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -89,20 +89,27 @@ class LocalCalendarEntity(CalendarEntity): self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - events = self._calendar.timeline_tz(start_date.tzinfo).overlapping( - start_date, - end_date, - ) - return [_get_calendar_event(event) for event in events] + + def events_in_range() -> list[CalendarEvent]: + events = self._calendar.timeline_tz(start_date.tzinfo).overlapping( + start_date, + end_date, + ) + return [_get_calendar_event(event) for event in events] + + return await self.hass.async_add_executor_job(events_in_range) async def async_update(self) -> None: """Update entity state with the next upcoming event.""" - now = dt_util.now() - events = self._calendar.timeline_tz(now.tzinfo).active_after(now) - if event := next(events, None): - self._event = _get_calendar_event(event) - else: - self._event = None + + def next_event() -> CalendarEvent | None: + now = dt_util.now() + events = self._calendar.timeline_tz(now.tzinfo).active_after(now) + if event := next(events, None): + return _get_calendar_event(event) + return None + + self._event = await self.hass.async_add_executor_job(next_event) async def _async_store(self) -> None: """Persist the calendar to disk.""" From 1f4cda6282e096c57f591dafefea0f702b3b17d1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:40:49 -0700 Subject: [PATCH 059/110] Bump ical to 9.2.0 (#144240) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 2bedc7a3163..32af3e675b3 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 90cd5a6d2ac..eba26e88d5a 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.1.0"] + "requirements": ["ical==9.2.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index a630c18c669..fb48ca72337 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==9.1.0"] + "requirements": ["ical==9.2.0"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index da078395484..b31fa3389dc 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.1.0"] + "requirements": ["ical==9.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 62cd159b4b7..6b46b27cee0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1200,7 +1200,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.1.0 +ical==9.2.0 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea6c7a12e25..de70195c2d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.1.0 +ical==9.2.0 # homeassistant.components.caldav icalendar==6.1.0 From 541506cbdbbf3699ea096a09e26c44bd1b28c3dd Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 5 May 2025 02:35:32 -0700 Subject: [PATCH 060/110] Fix Invalid statistic_id for Opower: National Grid (#144243) Co-authored-by: J. Nick Koston --- homeassistant/components/opower/coordinator.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index ff0e3264b48..d03c30b7db0 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -113,14 +113,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): _LOGGER.error("Error getting accounts: %s", err) raise for account in accounts: - id_prefix = "_".join( + id_prefix = ( ( - self.api.utility.subdomain(), - account.meter_type.name.lower(), - # Some utilities like AEP have "-" in their account id. - # Replace it with "_" to avoid "Invalid statistic_id" - account.utility_account_id.replace("-", "_").lower(), + f"{self.api.utility.subdomain()}_{account.meter_type.name}_" + f"{account.utility_account_id}" ) + # Some utilities like AEP have "-" in their account id. + # Other utilities like ngny-gas have "-" in their subdomain. + # Replace it with "_" to avoid "Invalid statistic_id" + .replace("-", "_") + .lower() ) cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost" compensation_statistic_id = f"{DOMAIN}:{id_prefix}_energy_compensation" From 56e895fdd47ff887eab41c85849ca520b1f10e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 5 May 2025 18:41:45 +0200 Subject: [PATCH 061/110] Remove program phase sensor from miele vacuum robot (#144257) * Use device class transation * Remove program pghses sensor from robot vacuum cleaner --- homeassistant/components/miele/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index b2ddd695042..0631d9c81dd 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -144,7 +144,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( MieleAppliance.STEAM_OVEN, MieleAppliance.MICROWAVE, MieleAppliance.COFFEE_SYSTEM, - MieleAppliance.ROBOT_VACUUM_CLEANER, MieleAppliance.WASHER_DRYER, MieleAppliance.STEAM_OVEN_COMBI, MieleAppliance.STEAM_OVEN_MICRO, From 3feda06e605c50db14ebb269d7b46568c81193cd Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sun, 4 May 2025 19:59:49 -0400 Subject: [PATCH 062/110] Bump python-roborock to 2.18.2 (#144235) --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 531590d5d6e..784d2c6ad27 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -19,7 +19,7 @@ "loggers": ["roborock"], "quality_scale": "silver", "requirements": [ - "python-roborock==2.16.1", + "python-roborock==2.18.2", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 6b46b27cee0..bd66a8ba3cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.16.1 +python-roborock==2.18.2 # homeassistant.components.smarttub python-smarttub==0.0.39 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de70195c2d0..4549421e39f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2017,7 +2017,7 @@ python-picnic-api2==1.2.4 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.16.1 +python-roborock==2.18.2 # homeassistant.components.smarttub python-smarttub==0.0.39 From 6247ec73a3edfc30c9aa45696bbd447f42f68b26 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 5 May 2025 10:40:48 -0400 Subject: [PATCH 063/110] Bump Roborock Map Parser to 0.1.4 (#144260) Bump to 0.1.4 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 784d2c6ad27..444232b5843 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -20,6 +20,6 @@ "quality_scale": "silver", "requirements": [ "python-roborock==2.18.2", - "vacuum-map-parser-roborock==0.1.2" + "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/requirements_all.txt b/requirements_all.txt index bd66a8ba3cc..2ada6e85491 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3007,7 +3007,7 @@ url-normalize==2.2.1 uvcclient==0.12.1 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.2 +vacuum-map-parser-roborock==0.1.4 # homeassistant.components.vallox vallox-websocket-api==5.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4549421e39f..70a70c14c42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2430,7 +2430,7 @@ url-normalize==2.2.1 uvcclient==0.12.1 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.2 +vacuum-map-parser-roborock==0.1.4 # homeassistant.components.vallox vallox-websocket-api==5.3.0 From e3a156c9b771b8fc2a238a46ceca10a6ad75a66f Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Mon, 5 May 2025 20:43:51 +0200 Subject: [PATCH 064/110] Bump pylamarzocco to 2.0.0 (#144275) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index ab5a77cad4c..572f70bc455 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.0b7"] + "requirements": ["pylamarzocco==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2ada6e85491..d8491ba3525 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b7 +pylamarzocco==2.0.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70a70c14c42..4ba1915ebf1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b7 +pylamarzocco==2.0.0 # homeassistant.components.lastfm pylast==5.1.0 From f7833bdbd44c2642c43ab1111cee6d222e8e09c0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 5 May 2025 20:47:15 +0200 Subject: [PATCH 065/110] Update frontend to 20250502.1 (#144276) --- 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 2cfa9572ff3..18e4d349122 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==20250502.0"] + "requirements": ["home-assistant-frontend==20250502.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 73415df8abd..a9788e03648 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250502.0 +home-assistant-frontend==20250502.1 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d8491ba3525..17ecf6fb4c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.0 +home-assistant-frontend==20250502.1 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ba1915ebf1..dc00d3c0c51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.0 +home-assistant-frontend==20250502.1 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From 379880255734a7301dd26b7a59443dd6678c576d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 May 2025 14:51:20 -0400 Subject: [PATCH 066/110] Bump version to 2025.5.0b4 --- 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 86caae51ea9..fd92fbb8325 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 600f8b0112c..d4b12cc72e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b3" +version = "2025.5.0b4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 0322dd0e0fba43edf2e48294c4936e41e0133f54 Mon Sep 17 00:00:00 2001 From: Jamin Date: Mon, 5 May 2025 19:25:52 -0500 Subject: [PATCH 067/110] Improve Voip pipeline stability (#137620) * Improve Voip pipeline stability It appears the pipeline is being unexpectedly cancelled in some instances. In order to mitigate this issue hang ups will be detected using a separate task rather than relying on timeouts in the STT read method. Also reading STT events will be retried once if it is cancelled. The pipeline will also catch and log any CancelledErrors to help with further debugging. * Update Voip tests * Remove unnecessary changes Remove unnecessary logging and cancelled error handling in wyoming STT. * Remove comment about clearing system prompt The test no longer checks for clearing the system prompt. Since that logic exists completely in the assist_satellite component I think it is reasonable to only test that logic in the unit tests for that component. * Re-raise cancellation Re-raise CancelledError if the current task is cancelling in the check hangup task Co-authored-by: J. Nick Koston * Re-raise CancelledError in pipeline as well * Fix formatting issue * Remove unnecessary logging * Add MockResultStream import to tests This was presumably missed while merging * Cancel check hangup task on disconnect * Add myself as codeowner for VoIP * Update CODEOWNERS --------- Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 4 +- .../components/voip/assist_satellite.py | 119 +++++++++--- homeassistant/components/voip/manifest.json | 2 +- tests/components/voip/test_voip.py | 171 ++++++++---------- 4 files changed, 171 insertions(+), 125 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 490f97879a4..6011445e603 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1678,8 +1678,8 @@ build.json @home-assistant/supervisor /tests/components/vlc_telnet/ @rodripf @MartinHjelmare /homeassistant/components/vodafone_station/ @paoloantinori @chemelli74 /tests/components/vodafone_station/ @paoloantinori @chemelli74 -/homeassistant/components/voip/ @balloob @synesthesiam -/tests/components/voip/ @balloob @synesthesiam +/homeassistant/components/voip/ @balloob @synesthesiam @jaminh +/tests/components/voip/ @balloob @synesthesiam @jaminh /homeassistant/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund /homeassistant/components/volvooncall/ @molobrakos diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py index a2364200ce2..7b34d7a11ba 100644 --- a/homeassistant/components/voip/assist_satellite.py +++ b/homeassistant/components/voip/assist_satellite.py @@ -51,9 +51,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) _PIPELINE_TIMEOUT_SEC: Final = 30 +_HANGUP_SEC: Final = 0.5 _ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5 _ANNOUNCEMENT_AFTER_DELAY: Final = 1.0 -_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5 _ANNOUNCEMENT_RING_TIMEOUT: Final = 30 @@ -132,9 +132,10 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol self._processing_tone_done = asyncio.Event() self._announcement: AssistSatelliteAnnouncement | None = None - self._announcement_future: asyncio.Future[Any] = asyncio.Future() self._announcment_start_time: float = 0.0 - self._check_announcement_ended_task: asyncio.Task | None = None + self._check_announcement_pickup_task: asyncio.Task | None = None + self._check_hangup_task: asyncio.Task | None = None + self._call_end_future: asyncio.Future[Any] = asyncio.Future() self._last_chunk_time: float | None = None self._rtp_port: int | None = None self._run_pipeline_after_announce: bool = False @@ -233,7 +234,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol translation_key="non_tts_announcement", ) - self._announcement_future = asyncio.Future() + self._call_end_future = asyncio.Future() self._run_pipeline_after_announce = run_pipeline_after if self._rtp_port is None: @@ -274,53 +275,77 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol rtp_port=self._rtp_port, ) - # Check if caller hung up or didn't pick up - self._check_announcement_ended_task = ( + # Check if caller didn't pick up + self._check_announcement_pickup_task = ( self.config_entry.async_create_background_task( self.hass, - self._check_announcement_ended(), - "voip_announcement_ended", + self._check_announcement_pickup(), + "voip_announcement_pickup", ) ) try: - await self._announcement_future + await self._call_end_future except TimeoutError: # Stop ringing + _LOGGER.debug("Caller did not pick up in time") sip_protocol.cancel_call(call_info) raise - async def _check_announcement_ended(self) -> None: + async def _check_announcement_pickup(self) -> None: """Continuously checks if an audio chunk was received within a time limit. - If not, the caller is presumed to have hung up and the announcement is ended. + If not, the caller is presumed to have not picked up the phone and the announcement is ended. """ - while self._announcement is not None: + while True: current_time = time.monotonic() if (self._last_chunk_time is None) and ( (current_time - self._announcment_start_time) > _ANNOUNCEMENT_RING_TIMEOUT ): # Ring timeout + _LOGGER.debug("Ring timeout") self._announcement = None - self._check_announcement_ended_task = None - self._announcement_future.set_exception( + self._check_announcement_pickup_task = None + self._call_end_future.set_exception( TimeoutError("User did not pick up in time") ) _LOGGER.debug("Timed out waiting for the user to pick up the phone") break - - if (self._last_chunk_time is not None) and ( - (current_time - self._last_chunk_time) > _ANNOUNCEMENT_HANGUP_SEC - ): - # Caller hung up - self._announcement = None - self._announcement_future.set_result(None) - self._check_announcement_ended_task = None - _LOGGER.debug("Announcement ended") + if self._last_chunk_time is not None: + _LOGGER.debug("Picked up the phone") + self._check_announcement_pickup_task = None break - await asyncio.sleep(_ANNOUNCEMENT_HANGUP_SEC / 2) + await asyncio.sleep(_HANGUP_SEC / 2) + + async def _check_hangup(self) -> None: + """Continuously checks if an audio chunk was received within a time limit. + + If not, the caller is presumed to have hung up and the call is ended. + """ + try: + while True: + current_time = time.monotonic() + if (self._last_chunk_time is not None) and ( + (current_time - self._last_chunk_time) > _HANGUP_SEC + ): + # Caller hung up + _LOGGER.debug("Hang up") + self._announcement = None + if self._run_pipeline_task is not None: + _LOGGER.debug("Cancelling running pipeline") + self._run_pipeline_task.cancel() + self._call_end_future.set_result(None) + self.disconnect() + break + + await asyncio.sleep(_HANGUP_SEC / 2) + except asyncio.CancelledError: + # Don't swallow cancellation + if (current_task := asyncio.current_task()) and current_task.cancelling(): + raise + _LOGGER.debug("Check hangup cancelled") async def async_start_conversation( self, start_announcement: AssistSatelliteAnnouncement @@ -332,6 +357,24 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol # VoIP # ------------------------------------------------------------------------- + def disconnect(self): + """Server disconnected.""" + super().disconnect() + if self._check_hangup_task is not None: + self._check_hangup_task.cancel() + self._check_hangup_task = None + + def connection_made(self, transport): + """Server is ready.""" + super().connection_made(transport) + self._last_chunk_time = time.monotonic() + # Check if caller hung up + self._check_hangup_task = self.config_entry.async_create_background_task( + self.hass, + self._check_hangup(), + "voip_hangup", + ) + def on_chunk(self, audio_bytes: bytes) -> None: """Handle raw audio chunk.""" self._last_chunk_time = time.monotonic() @@ -368,13 +411,22 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol self.voip_device.set_is_active(True) async def stt_stream(): + retry: bool = True while True: - async with asyncio.timeout(self._audio_chunk_timeout): - chunk = await self._audio_queue.get() - if not chunk: - break + try: + async with asyncio.timeout(self._audio_chunk_timeout): + chunk = await self._audio_queue.get() + if not chunk: + _LOGGER.debug("STT stream got None") + break yield chunk + except TimeoutError: + _LOGGER.debug("STT Stream timed out") + if not retry: + _LOGGER.debug("No more retries, ending STT stream") + break + retry = False # Play listening tone at the start of each cycle await self._play_tone(Tones.LISTENING, silence_before=0.2) @@ -385,6 +437,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol ) if self._pipeline_had_error: + _LOGGER.debug("Pipeline error") self._pipeline_had_error = False await self._play_tone(Tones.ERROR) else: @@ -394,7 +447,14 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol # length of the TTS audio. await self._tts_done.wait() except TimeoutError: + # This shouldn't happen anymore, we are detecting hang ups with a separate task + _LOGGER.exception("Timeout error") self.disconnect() # caller hung up + except asyncio.CancelledError: + _LOGGER.debug("Pipeline cancelled") + # Don't swallow cancellation + if (current_task := asyncio.current_task()) and current_task.cancelling(): + raise finally: # Stop audio stream await self._audio_queue.put(None) @@ -433,8 +493,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol if self._run_pipeline_after_announce: # Clear announcement to allow pipeline to run + _LOGGER.debug("Clearing announcement") self._announcement = None - self._announcement_future.set_result(None) def _clear_audio_queue(self) -> None: """Ensure audio queue is empty.""" @@ -463,6 +523,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol ) else: # Empty TTS response + _LOGGER.debug("Empty TTS response") self._tts_done.set() elif event.type == PipelineEventType.ERROR: # Play error tone instead of wait for TTS when pipeline is finished. diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index dfd397fde14..09e1f112699 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -1,7 +1,7 @@ { "domain": "voip", "name": "Voice over IP", - "codeowners": ["@balloob", "@synesthesiam"], + "codeowners": ["@balloob", "@synesthesiam", "@jaminh"], "config_flow": true, "dependencies": ["assist_pipeline", "assist_satellite", "intent", "network"], "documentation": "https://www.home-assistant.io/integrations/voip", diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 345f0399645..65567c8e1d1 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -335,9 +335,8 @@ async def test_pipeline( patch.object(satellite, "tts_response_finished", tts_response_finished), ): satellite._tones = Tones(0) - satellite.transport = Mock() + satellite.connection_made(Mock()) - satellite.connection_made(satellite.transport) assert satellite.state == AssistSatelliteState.IDLE # Ensure audio queue is cleared before pipeline starts @@ -473,7 +472,7 @@ async def test_tts_timeout( for tone in Tones: satellite._tone_bytes[tone] = tone_bytes - satellite.transport = Mock() + satellite.connection_made(Mock()) satellite.send_audio = Mock() original_send_tts = satellite._send_tts @@ -511,6 +510,7 @@ async def test_tts_wrong_extension( assert await async_setup_component(hass, "voip", {}) satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) done = asyncio.Event() @@ -559,8 +559,6 @@ async def test_tts_wrong_extension( "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - satellite.transport = Mock() - original_send_tts = satellite._send_tts async def send_tts(*args, **kwargs): @@ -572,6 +570,8 @@ async def test_tts_wrong_extension( satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] + satellite.connection_made(Mock()) + # silence satellite.on_chunk(bytes(_ONE_SECOND)) @@ -579,10 +579,18 @@ async def test_tts_wrong_extension( satellite.on_chunk(bytes([255] * _ONE_SECOND * 2)) # silence (assumes relaxed VAD sensitivity) - satellite.on_chunk(bytes(_ONE_SECOND * 4)) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to exhaust the audio stream - async with asyncio.timeout(1): + async with asyncio.timeout(3): await done.wait() @@ -595,6 +603,7 @@ async def test_tts_wrong_wav_format( assert await async_setup_component(hass, "voip", {}) satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) done = asyncio.Event() @@ -643,8 +652,6 @@ async def test_tts_wrong_wav_format( "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - satellite.transport = Mock() - original_send_tts = satellite._send_tts async def send_tts(*args, **kwargs): @@ -656,6 +663,8 @@ async def test_tts_wrong_wav_format( satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] + satellite.connection_made(Mock()) + # silence satellite.on_chunk(bytes(_ONE_SECOND)) @@ -663,10 +672,18 @@ async def test_tts_wrong_wav_format( satellite.on_chunk(bytes([255] * _ONE_SECOND * 2)) # silence (assumes relaxed VAD sensitivity) - satellite.on_chunk(bytes(_ONE_SECOND * 4)) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to exhaust the audio stream - async with asyncio.timeout(1): + async with asyncio.timeout(3): await done.wait() @@ -679,6 +696,7 @@ async def test_empty_tts_output( assert await async_setup_component(hass, "voip", {}) satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) async def async_pipeline_from_audio_stream(*args, **kwargs): @@ -728,7 +746,7 @@ async def test_empty_tts_output( "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", ) as mock_send_tts, ): - satellite.transport = Mock() + satellite.connection_made(Mock()) # silence satellite.on_chunk(bytes(_ONE_SECOND)) @@ -737,10 +755,18 @@ async def test_empty_tts_output( satellite.on_chunk(bytes([255] * _ONE_SECOND * 2)) # silence (assumes relaxed VAD sensitivity) - satellite.on_chunk(bytes(_ONE_SECOND * 4)) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to finish - async with asyncio.timeout(1): + async with asyncio.timeout(2): await satellite._tts_done.wait() mock_send_tts.assert_not_called() @@ -785,7 +811,7 @@ async def test_pipeline_error( ), ): satellite._tones = Tones.ERROR - satellite.transport = Mock() + satellite.connection_made(Mock()) satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign] satellite.on_chunk(bytes(_ONE_SECOND)) @@ -845,16 +871,20 @@ async def test_announce( "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", ) as mock_send_tts, ): - satellite.transport = Mock() announce_task = hass.async_create_background_task( satellite.async_announce(announcement), "voip_announce" ) await asyncio.sleep(0) + satellite.connection_made(Mock()) mock_protocol.outgoing_call.assert_called_once() # Trigger announcement satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(2): await announce_task mock_send_tts.assert_called_once_with( @@ -897,11 +927,11 @@ async def test_voip_id_is_ip_address( "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", ) as mock_send_tts, ): - satellite.transport = Mock() announce_task = hass.async_create_background_task( satellite.async_announce(announcement), "voip_announce" ) await asyncio.sleep(0) + satellite.connection_made(Mock()) mock_protocol.outgoing_call.assert_called_once() assert ( mock_protocol.outgoing_call.call_args.kwargs["destination"].host @@ -910,7 +940,11 @@ async def test_voip_id_is_ip_address( # Trigger announcement satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(2): await announce_task mock_send_tts.assert_called_once_with( @@ -955,7 +989,7 @@ async def test_announce_timeout( 0.01, ), ): - satellite.transport = Mock() + satellite.connection_made(Mock()) with pytest.raises(TimeoutError): await satellite.async_announce(announcement) @@ -1042,7 +1076,7 @@ async def test_start_conversation( new=async_pipeline_from_audio_stream, ), ): - satellite.transport = Mock() + satellite.connection_made(Mock()) conversation_task = hass.async_create_background_task( satellite.async_start_conversation(announcement), "voip_start_conversation" ) @@ -1051,16 +1085,20 @@ async def test_start_conversation( # Trigger announcement and wait for it to finish satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(2): await tts_sent.wait() - tts_sent.clear() - # Trigger pipeline satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): - # Wait for TTS - await tts_sent.wait() + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(3) + async with asyncio.timeout(3): + # Wait for Conversation end await conversation_task @@ -1073,21 +1111,8 @@ async def test_start_conversation_user_doesnt_pick_up( """Test start conversation when the user doesn't pick up.""" assert await async_setup_component(hass, "voip", {}) - pipeline = assist_pipeline.Pipeline( - conversation_engine="test engine", - conversation_language="en", - language="en", - name="test pipeline", - stt_engine="test stt", - stt_language="en", - tts_engine="test tts", - tts_language="en", - tts_voice=None, - wake_word_entity=None, - wake_word_id=None, - ) - satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) assert ( satellite.supported_features @@ -1098,62 +1123,22 @@ async def test_start_conversation_user_doesnt_pick_up( mock_protocol: AsyncMock = hass.data[DOMAIN].protocol mock_protocol.outgoing_call = Mock() - pipeline_started = asyncio.Event() - - async def async_pipeline_from_audio_stream( - hass: HomeAssistant, - context: Context, - *args, - conversation_extra_system_prompt: str | None = None, - **kwargs, - ): - # System prompt should be not be set due to timeout (user not picking up) - assert conversation_extra_system_prompt is None - - pipeline_started.set() + announcement = assist_satellite.AssistSatelliteAnnouncement( + message="test announcement", + media_id=_MEDIA_ID, + tts_token="test-token", + original_media_id=_MEDIA_ID, + media_id_source="tts", + ) + # Very short timeout which will trigger because we don't send any audio in with ( patch( - "homeassistant.components.assist_satellite.entity.async_get_pipeline", - return_value=pipeline, - ), - patch( - "homeassistant.components.voip.assist_satellite.VoipAssistSatellite.async_start_conversation", - side_effect=TimeoutError, - ), - patch( - "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), - patch( - "homeassistant.components.tts.generate_media_source_id", - return_value="media-source://bla", - ), - patch( - "homeassistant.components.tts.async_resolve_engine", - return_value="test tts", - ), - patch( - "homeassistant.components.tts.async_create_stream", - return_value=MockResultStream(hass, "wav", b""), + "homeassistant.components.voip.assist_satellite._ANNOUNCEMENT_RING_TIMEOUT", + 0.1, ), ): - satellite.transport = Mock() + satellite.connection_made(Mock()) - # Error should clear system prompt with pytest.raises(TimeoutError): - await hass.services.async_call( - assist_satellite.DOMAIN, - "start_conversation", - { - "entity_id": satellite.entity_id, - "start_message": "test announcement", - "extra_system_prompt": "test prompt", - }, - blocking=True, - ) - - # Trigger a pipeline so we can check if the system prompt was cleared - satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): - await pipeline_started.wait() + await satellite.async_start_conversation(announcement) From 38f26376a172540b350eb0eb3579a87b8f091123 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 May 2025 21:26:56 +0200 Subject: [PATCH 068/110] Fix default entity name not the device default entity when no name set on MQTT subentry entity (#144263) --- homeassistant/components/mqtt/config_flow.py | 12 +++++++++--- tests/components/mqtt/common.py | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 1f317d9f743..2bbfd9e3515 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -465,7 +465,7 @@ class PlatformField: required: bool validator: Callable[..., Any] error: str | None = None - default: str | int | bool | vol.Undefined = vol.UNDEFINED + default: str | int | bool | None | vol.Undefined = vol.UNDEFINED is_schema_default: bool = False exclude_from_reconfig: bool = False conditions: tuple[dict[str, Any], ...] | None = None @@ -515,6 +515,7 @@ COMMON_ENTITY_FIELDS = { required=False, validator=str, exclude_from_reconfig=True, + default=None, ), CONF_ENTITY_PICTURE: PlatformField( selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url" @@ -1324,7 +1325,10 @@ def data_schema_from_fields( vol.Required(field_name, default=field_details.default) if field_details.required else vol.Optional( - field_name, default=field_details.default + field_name, + default=field_details.default + if field_details.default is not None + else vol.UNDEFINED, ): field_details.selector(component_data_with_user_input) # type: ignore[operator] if field_details.custom_filtering else field_details.selector @@ -1375,12 +1379,14 @@ def data_schema_from_fields( @callback def subentry_schema_default_data_from_fields( data_schema_fields: dict[str, PlatformField], + component_data: dict[str, Any], ) -> dict[str, Any]: """Generate custom data schema from platform fields or device data.""" return { key: field.default for key, field in data_schema_fields.items() if field.is_schema_default + or (field.default is not vol.UNDEFINED and key not in component_data) } @@ -2206,7 +2212,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): for component_data in self._subentry_data["components"].values(): platform = component_data[CONF_PLATFORM] subentry_default_data = subentry_schema_default_data_from_fields( - PLATFORM_ENTITY_FIELDS[platform] + PLATFORM_ENTITY_FIELDS[platform] | COMMON_ENTITY_FIELDS, component_data ) component_data.update(subentry_default_data) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index d811b601036..4e402046e2c 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -87,6 +87,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = { MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = { "5269352dd9534c908d22812ea5d714cd": { "platform": "notify", + "name": None, "command_topic": "test-topic", "command_template": "{{ value }}", "entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd", From 283e9d073b839034967cc4ddc678f89f4cfe72db Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 5 May 2025 21:02:20 +0200 Subject: [PATCH 069/110] Fix Z-Wave config flow forms (#144279) --- .../components/zwave_js/config_flow.py | 8 +++-- .../components/zwave_js/strings.json | 18 +++++++---- tests/components/zwave_js/test_config_flow.py | 32 +++++++++---------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 2d9bc0fa1cd..184a7724799 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -717,7 +717,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): data_schema = vol.Schema(schema) - return self.async_show_form(step_id="configure_addon", data_schema=data_schema) + return self.async_show_form( + step_id="configure_addon_user", data_schema=data_schema + ) async def async_step_finish_addon_setup_user( self, user_input: dict[str, Any] | None = None @@ -1097,7 +1099,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): } ) - return self.async_show_form(step_id="configure_addon", data_schema=data_schema) + return self.async_show_form( + step_id="configure_addon_reconfigure", data_schema=data_schema + ) async def async_step_choose_serial_port( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 53615e84691..56ae4e12401 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -37,8 +37,10 @@ "restore_nvm": "Please wait while the network restore completes." }, "step": { - "configure_addon": { + "configure_addon_user": { "data": { + "lr_s2_access_control_key": "Long Range S2 Access Control Key", + "lr_s2_authenticated_key": "Long Range S2 Authenticated Key", "s0_legacy_key": "S0 Key (Legacy)", "s2_access_control_key": "S2 Access Control Key", "s2_authenticated_key": "S2 Authenticated Key", @@ -52,14 +54,16 @@ "data": { "emulate_hardware": "Emulate Hardware", "log_level": "Log level", - "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]", - "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]", - "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]", - "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]", + "lr_s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_access_control_key%]", + "lr_s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_authenticated_key%]", + "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s0_legacy_key%]", + "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_access_control_key%]", + "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_authenticated_key%]", + "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_unauthenticated_key%]", "usb_path": "[%key:common::config_flow::data::usb_path%]" }, - "description": "[%key:component::zwave_js::config::step::configure_addon::description%]", - "title": "[%key:component::zwave_js::config::step::configure_addon::title%]" + "description": "[%key:component::zwave_js::config::step::configure_addon_user::description%]", + "title": "[%key:component::zwave_js::config::step::configure_addon_user::title%]" }, "hassio_confirm": { "description": "Do you want to set up the Z-Wave integration with the Z-Wave add-on?" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 1d8b997ea4d..3778e36f897 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -682,7 +682,7 @@ async def test_usb_discovery( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -783,7 +783,7 @@ async def test_usb_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" # Make sure the discovered usb device is preferred. data_schema = result["data_schema"] @@ -1015,7 +1015,7 @@ async def test_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1117,7 +1117,7 @@ async def test_discovery_addon_not_installed( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1674,7 +1674,7 @@ async def test_addon_installed( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1777,7 +1777,7 @@ async def test_addon_installed_start_failure( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1862,7 +1862,7 @@ async def test_addon_installed_failures( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1943,7 +1943,7 @@ async def test_addon_installed_set_options_failure( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2058,7 +2058,7 @@ async def test_addon_installed_already_configured( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2154,7 +2154,7 @@ async def test_addon_not_installed( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2600,7 +2600,7 @@ async def test_reconfigure_addon_running( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2735,7 +2735,7 @@ async def test_reconfigure_addon_running_no_changes( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2916,7 +2916,7 @@ async def test_reconfigure_different_device( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3099,7 +3099,7 @@ async def test_reconfigure_addon_restart_failed( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3240,7 +3240,7 @@ async def test_reconfigure_addon_running_server_info_failure( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3387,7 +3387,7 @@ async def test_reconfigure_addon_not_installed( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], From 867df993531a9def03ac9641d1077fcac4f12b68 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 6 May 2025 04:39:25 +0200 Subject: [PATCH 070/110] Fix un-/re-load of Feedreader integration (#144285) fix unload platforms call --- homeassistant/components/feedreader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 31617cb220b..57c58d3a2b1 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -45,7 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry) # if this is the last entry, remove the storage if len(entries) == 1: hass.data.pop(MY_KEY) - return await hass.config_entries.async_unload_platforms(entry, Platform.EVENT) + return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT]) async def _async_update_listener( From 7f7a33b0275c844dab33853afb926fb8fb5acf0f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 May 2025 04:39:03 +0200 Subject: [PATCH 071/110] Fix mqtt subentry device name is not required but should be (#144289) Fix mqtt subentry device name is not required --- homeassistant/components/mqtt/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 2bbfd9e3515..02c8a1cdc8a 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1151,7 +1151,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ } MQTT_DEVICE_PLATFORM_FIELDS = { - ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str), + ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True, validator=str), ATTR_SW_VERSION: PlatformField( selector=TEXT_SELECTOR, required=False, validator=str ), From 86162eb6609357626510a70a3f05940b16ba28de Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Mon, 5 May 2025 22:37:54 -0400 Subject: [PATCH 072/110] Rehlko adjust timeouts for coordinator polls (#144297) --- homeassistant/components/rehlko/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index 49ceb8ac870..bda2704a206 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -29,6 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo """Set up Rehlko from a config entry.""" websession = async_get_clientsession(hass) rehlko = AioKem(session=websession) + # If requests take more than 20 seconds; timeout and let the setup retry. + rehlko.set_timeout(20) async def async_refresh_token_update(refresh_token: str) -> None: """Handle refresh token update.""" @@ -87,6 +89,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Retrys enabled after successful connection to prevent blocking startup rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20]) + # Rehlko service can be slow to respond, increase timeout for polls. + rehlko.set_timeout(100) return True From 46ef5789869eeda293b0d20ec099d55a222c89d9 Mon Sep 17 00:00:00 2001 From: Jamin Date: Mon, 5 May 2025 21:50:46 -0500 Subject: [PATCH 073/110] Bump VoIP utils to 0.3.2 (#144298) --- homeassistant/components/voip/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index 09e1f112699..59e54bfefea 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["voip_utils"], "quality_scale": "internal", - "requirements": ["voip-utils==0.3.1"] + "requirements": ["voip-utils==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17ecf6fb4c9..aeeb831f517 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3025,7 +3025,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.1 +voip-utils==0.3.2 # homeassistant.components.volkszaehler volkszaehler==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc00d3c0c51..b3c03e547c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2448,7 +2448,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.1 +voip-utils==0.3.2 # homeassistant.components.volvooncall volvooncall==0.10.3 From 918499a85c22f1b4b0240a1f018c5bb1e9a35964 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 May 2025 02:54:14 +0000 Subject: [PATCH 074/110] Bump version to 2025.5.0b5 --- 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 fd92fbb8325..78761216104 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index d4b12cc72e5..7a63cdc61dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b4" +version = "2025.5.0b5" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 576b4ef60d4f5ea2d6405786e684db837da675ce Mon Sep 17 00:00:00 2001 From: Cerallin <66366855+Cerallin@users.noreply.github.com> Date: Tue, 6 May 2025 15:55:43 +0800 Subject: [PATCH 075/110] Bump xiaomi-ble to 0.38.0 (#143885) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index a908d4747ad..3f13c7921a8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.37.0"] + "requirements": ["xiaomi-ble==0.38.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index aeeb831f517..f0e7f3b2464 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3101,7 +3101,7 @@ wyoming==1.5.4 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.37.0 +xiaomi-ble==0.38.0 # homeassistant.components.knx xknx==3.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3c03e547c9..dc8f334698a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2509,7 +2509,7 @@ wyoming==1.5.4 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.37.0 +xiaomi-ble==0.38.0 # homeassistant.components.knx xknx==3.6.0 From a91ae71139883ebd66a4e75e7f9004a51ade6ee4 Mon Sep 17 00:00:00 2001 From: Ivan Lopez Hernandez Date: Mon, 5 May 2025 23:45:39 -0700 Subject: [PATCH 076/110] Fixes #140182 by checking file status before sending the prompt. (#144131) * Added unit tests * Addressed review comments * Fixed tests * PR comments --- .../__init__.py | 36 ++++++ .../const.py | 1 + .../snapshots/test_init.ambr | 17 +++ .../test_init.py | 112 ++++++++++++++++++ 4 files changed, 166 insertions(+) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 88a51446cda..79d092a60c3 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -2,11 +2,13 @@ from __future__ import annotations +import asyncio import mimetypes from pathlib import Path from google.genai import Client from google.genai.errors import APIError, ClientError +from google.genai.types import File, FileState from requests.exceptions import Timeout import voluptuous as vol @@ -32,6 +34,8 @@ from .const import ( CONF_CHAT_MODEL, CONF_PROMPT, DOMAIN, + FILE_POLLING_INTERVAL_SECONDS, + LOGGER, RECOMMENDED_CHAT_MODEL, TIMEOUT_MILLIS, ) @@ -91,8 +95,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) prompt_parts.append(uploaded_file) + async def wait_for_file_processing(uploaded_file: File) -> None: + """Wait for file processing to complete.""" + while True: + uploaded_file = await client.aio.files.get( + name=uploaded_file.name, + config={"http_options": {"timeout": TIMEOUT_MILLIS}}, + ) + if uploaded_file.state not in ( + FileState.STATE_UNSPECIFIED, + FileState.PROCESSING, + ): + break + LOGGER.debug( + "Waiting for file `%s` to be processed, current state: %s", + uploaded_file.name, + uploaded_file.state, + ) + await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS) + + if uploaded_file.state == FileState.FAILED: + raise HomeAssistantError( + f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}" + ) + await hass.async_add_executor_job(append_files_to_prompt) + tasks = [ + asyncio.create_task(wait_for_file_processing(part)) + for part in prompt_parts + if isinstance(part, File) and part.state != FileState.ACTIVE + ] + async with asyncio.timeout(TIMEOUT_MILLIS / 1000): + await asyncio.gather(*tasks) + try: response = await client.aio.models.generate_content( model=RECOMMENDED_CHAT_MODEL, contents=prompt_parts diff --git a/homeassistant/components/google_generative_ai_conversation/const.py b/homeassistant/components/google_generative_ai_conversation/const.py index a7dd584ebee..239b3ff763e 100644 --- a/homeassistant/components/google_generative_ai_conversation/const.py +++ b/homeassistant/components/google_generative_ai_conversation/const.py @@ -26,3 +26,4 @@ CONF_USE_GOOGLE_SEARCH_TOOL = "enable_google_search_tool" RECOMMENDED_USE_GOOGLE_SEARCH_TOOL = False TIMEOUT_MILLIS = 10000 +FILE_POLLING_INTERVAL_SECONDS = 0.05 diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index ce882adf6e6..d8e54b15f61 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -1,4 +1,21 @@ # serializer version: 1 +# name: test_generate_content_file_processing_succeeds + list([ + tuple( + '', + tuple( + ), + dict({ + 'contents': list([ + 'Describe this image from my doorbell camera', + File(name='doorbell_snapshot.jpg', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), + File(name='context.txt', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), + ]), + 'model': 'models/gemini-2.0-flash', + }), + ), + ]) +# --- # name: test_generate_content_service_with_image list([ tuple( diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index a08acc0df3f..94308260f74 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock, mock_open, patch +from google.genai.types import File, FileState import pytest from requests.exceptions import Timeout from syrupy.assertion import SnapshotAssertion @@ -91,6 +92,117 @@ async def test_generate_content_service_with_image( assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot +@pytest.mark.usefixtures("mock_init_component") +async def test_generate_content_file_processing_succeeds( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test generate content service.""" + stubbed_generated_content = ( + "A mail carrier is at your front door delivering a package" + ) + + with ( + patch( + "google.genai.models.AsyncModels.generate_content", + return_value=Mock( + text=stubbed_generated_content, + prompt_feedback=None, + candidates=[Mock()], + ), + ) as mock_generate, + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("builtins.open", mock_open(read_data="this is an image")), + patch("mimetypes.guess_type", return_value=["image/jpeg"]), + patch( + "google.genai.files.Files.upload", + side_effect=[ + File(name="doorbell_snapshot.jpg", state=FileState.ACTIVE), + File(name="context.txt", state=FileState.PROCESSING), + ], + ), + patch( + "google.genai.files.AsyncFiles.get", + side_effect=[ + File(name="context.txt", state=FileState.PROCESSING), + File(name="context.txt", state=FileState.ACTIVE), + ], + ), + ): + response = await hass.services.async_call( + "google_generative_ai_conversation", + "generate_content", + { + "prompt": "Describe this image from my doorbell camera", + "filenames": ["doorbell_snapshot.jpg", "context.txt", "context.txt"], + }, + blocking=True, + return_response=True, + ) + + assert response == { + "text": stubbed_generated_content, + } + assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot + + +@pytest.mark.usefixtures("mock_init_component") +async def test_generate_content_file_processing_fails( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test generate content service.""" + stubbed_generated_content = ( + "A mail carrier is at your front door delivering a package" + ) + + with ( + patch( + "google.genai.models.AsyncModels.generate_content", + return_value=Mock( + text=stubbed_generated_content, + prompt_feedback=None, + candidates=[Mock()], + ), + ), + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("builtins.open", mock_open(read_data="this is an image")), + patch("mimetypes.guess_type", return_value=["image/jpeg"]), + patch( + "google.genai.files.Files.upload", + side_effect=[ + File(name="doorbell_snapshot.jpg", state=FileState.ACTIVE), + File(name="context.txt", state=FileState.PROCESSING), + ], + ), + patch( + "google.genai.files.AsyncFiles.get", + side_effect=[ + File(name="context.txt", state=FileState.PROCESSING), + File( + name="context.txt", + state=FileState.FAILED, + error={"message": "File processing failed"}, + ), + ], + ), + pytest.raises( + HomeAssistantError, + match="File `context.txt` processing failed, reason: File processing failed", + ), + ): + await hass.services.async_call( + "google_generative_ai_conversation", + "generate_content", + { + "prompt": "Describe this image from my doorbell camera", + "filenames": ["doorbell_snapshot.jpg", "context.txt", "context.txt"], + }, + blocking=True, + return_response=True, + ) + + @pytest.mark.usefixtures("mock_init_component") async def test_generate_content_service_error( hass: HomeAssistant, From 58f7a8a51eddfd191fb3daa39bb2af6a81e24865 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 May 2025 10:33:58 +0200 Subject: [PATCH 077/110] Fix Z-Wave USB discovery to use serial by id path (#144314) --- homeassistant/components/zwave_js/config_flow.py | 10 +++++++++- tests/components/zwave_js/test_config_flow.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 184a7724799..c6624046a00 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -461,10 +461,18 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): if vid == "10C4" and pid == "EA60" and description and "2652" in description: return self.async_abort(reason="not_zwave_device") + discovery_info.device = await self.hass.async_add_executor_job( + usb.get_serial_by_id, discovery_info.device + ) + addon_info = await self._async_get_addon_info() if ( addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.INSTALLING) - and addon_info.options.get(CONF_ADDON_DEVICE) == discovery_info.device + and (addon_device := addon_info.options.get(CONF_ADDON_DEVICE)) is not None + and await self.hass.async_add_executor_job( + usb.get_serial_by_id, addon_device + ) + == discovery_info.device ): return self.async_abort(reason="already_configured") diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3778e36f897..08f0ffad4bd 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -653,6 +653,7 @@ async def test_usb_discovery( install_addon, addon_options, get_addon_discovery_info, + mock_usb_serial_by_id: MagicMock, set_addon_options, start_addon, usb_discovery_info: UsbServiceInfo, @@ -668,6 +669,7 @@ async def test_usb_discovery( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -765,6 +767,7 @@ async def test_usb_discovery_addon_not_running( supervisor, addon_installed, addon_options, + mock_usb_serial_by_id: MagicMock, set_addon_options, start_addon, get_addon_discovery_info, @@ -779,6 +782,7 @@ async def test_usb_discovery_addon_not_running( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -876,6 +880,7 @@ async def test_usb_discovery_addon_not_running( async def test_usb_discovery_migration( hass: HomeAssistant, addon_options: dict[str, Any], + mock_usb_serial_by_id: MagicMock, set_addon_options: AsyncMock, restart_addon: AsyncMock, client: MagicMock, @@ -929,6 +934,7 @@ async def test_usb_discovery_migration( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -1278,6 +1284,7 @@ async def test_abort_usb_discovery_addon_required( async def test_abort_usb_discovery_confirm_addon_required( hass: HomeAssistant, addon_options: dict[str, Any], + mock_usb_serial_by_id: MagicMock, ) -> None: """Test usb discovery confirm aborted when existing entry not using add-on.""" addon_options["device"] = "/dev/another_device" @@ -1301,6 +1308,7 @@ async def test_abort_usb_discovery_confirm_addon_required( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 hass.config_entries.async_update_entry( entry, @@ -1331,6 +1339,7 @@ async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None: async def test_usb_discovery_same_device( hass: HomeAssistant, addon_options: dict[str, Any], + mock_usb_serial_by_id: MagicMock, ) -> None: """Test usb discovery flow is aborted when the add-on device is discovered.""" addon_options["device"] = USB_DISCOVERY_INFO.device @@ -1341,6 +1350,7 @@ async def test_usb_discovery_same_device( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + assert mock_usb_serial_by_id.call_count == 2 @pytest.mark.parametrize( From 5f70140e72e6cf56c486b2ce6dc303cc502c39f5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 May 2025 12:01:27 +0200 Subject: [PATCH 078/110] Revert "Disable S3 checksums" (#144092) (#144318) --- homeassistant/components/s3/__init__.py | 7 ------- tests/components/s3/test_init.py | 17 ----------------- 2 files changed, 24 deletions(-) diff --git a/homeassistant/components/s3/__init__.py b/homeassistant/components/s3/__init__.py index ea6b8e244b1..95e5e7d738c 100644 --- a/homeassistant/components/s3/__init__.py +++ b/homeassistant/components/s3/__init__.py @@ -7,7 +7,6 @@ from typing import cast from aiobotocore.client import AioBaseClient as S3Client from aiobotocore.session import AioSession -from botocore.config import Config from botocore.exceptions import ClientError, ConnectionError, ParamValidationError from homeassistant.config_entries import ConfigEntry @@ -33,11 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: """Set up S3 from a config entry.""" data = cast(dict, entry.data) - # due to https://github.com/home-assistant/core/issues/143995 - config = Config( - request_checksum_calculation="when_required", - response_checksum_validation="when_required", - ) try: session = AioSession() # pylint: disable-next=unnecessary-dunder-call @@ -46,7 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: endpoint_url=data.get(CONF_ENDPOINT_URL), aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY], aws_access_key_id=data[CONF_ACCESS_KEY_ID], - config=config, ).__aenter__() await client.head_bucket(Bucket=data[CONF_BUCKET]) except ClientError as err: diff --git a/tests/components/s3/test_init.py b/tests/components/s3/test_init.py index 8255bbd0c66..afa11f5cf72 100644 --- a/tests/components/s3/test_init.py +++ b/tests/components/s3/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -from botocore.config import Config from botocore.exceptions import ( ClientError, EndpointConnectionError, @@ -74,19 +73,3 @@ async def test_setup_entry_head_bucket_error( ) await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_checksum_settings_present( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test that checksum validation is set to be compatible with third-party S3 providers.""" - # due to https://github.com/home-assistant/core/issues/143995 - with patch( - "homeassistant.components.s3.AioSession.create_client" - ) as mock_create_client: - await setup_integration(hass, mock_config_entry) - - config_arg = mock_create_client.call_args[1]["config"] - assert isinstance(config_arg, Config) - assert config_arg.request_checksum_calculation == "when_required" - assert config_arg.response_checksum_validation == "when_required" From 1aa79c71cc8421abb0f4ac67fd6c3bdc13de5b6e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 May 2025 13:29:37 +0200 Subject: [PATCH 079/110] Rename S3 to AWS_S3 (#144324) --- CODEOWNERS | 4 ++-- homeassistant/brands/amazon.json | 9 ++++++++- homeassistant/components/{s3 => aws_s3}/__init__.py | 2 +- homeassistant/components/{s3 => aws_s3}/backup.py | 2 +- .../components/{s3 => aws_s3}/config_flow.py | 2 +- homeassistant/components/{s3 => aws_s3}/const.py | 4 ++-- .../components/{s3 => aws_s3}/manifest.json | 6 +++--- .../components/{s3 => aws_s3}/quality_scale.yaml | 0 homeassistant/components/{s3 => aws_s3}/strings.json | 12 ++++++------ homeassistant/generated/config_flows.py | 2 +- homeassistant/generated/integrations.json | 12 ++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/{s3 => aws_s3}/__init__.py | 2 +- tests/components/{s3 => aws_s3}/conftest.py | 8 ++++---- tests/components/{s3 => aws_s3}/const.py | 4 ++-- tests/components/{s3 => aws_s3}/test_backup.py | 10 +++++----- tests/components/{s3 => aws_s3}/test_config_flow.py | 4 ++-- tests/components/{s3 => aws_s3}/test_init.py | 2 +- 19 files changed, 48 insertions(+), 41 deletions(-) rename homeassistant/components/{s3 => aws_s3}/__init__.py (98%) rename homeassistant/components/{s3 => aws_s3}/backup.py (99%) rename homeassistant/components/{s3 => aws_s3}/config_flow.py (98%) rename homeassistant/components/{s3 => aws_s3}/const.py (90%) rename homeassistant/components/{s3 => aws_s3}/manifest.json (66%) rename homeassistant/components/{s3 => aws_s3}/quality_scale.yaml (100%) rename homeassistant/components/{s3 => aws_s3}/strings.json (73%) rename tests/components/{s3 => aws_s3}/__init__.py (90%) rename tests/components/{s3 => aws_s3}/conftest.py (93%) rename tests/components/{s3 => aws_s3}/const.py (78%) rename tests/components/{s3 => aws_s3}/test_backup.py (98%) rename tests/components/{s3 => aws_s3}/test_config_flow.py (96%) rename tests/components/{s3 => aws_s3}/test_init.py (98%) diff --git a/CODEOWNERS b/CODEOWNERS index 6011445e603..8fb77243bd1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -171,6 +171,8 @@ build.json @home-assistant/supervisor /homeassistant/components/avea/ @pattyland /homeassistant/components/awair/ @ahayworth @danielsjf /tests/components/awair/ @ahayworth @danielsjf +/homeassistant/components/aws_s3/ @tomasbedrich +/tests/components/aws_s3/ @tomasbedrich /homeassistant/components/axis/ @Kane610 /tests/components/axis/ @Kane610 /homeassistant/components/azure_data_explorer/ @kaareseras @@ -1318,8 +1320,6 @@ build.json @home-assistant/supervisor /tests/components/ruuvitag_ble/ @akx /homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc /tests/components/rympro/ @OnFreund @elad-bar @maorcc -/homeassistant/components/s3/ @tomasbedrich -/tests/components/s3/ @tomasbedrich /homeassistant/components/sabnzbd/ @shaiu @jpbede /tests/components/sabnzbd/ @shaiu @jpbede /homeassistant/components/saj/ @fredericvl diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index a7caea2b932..624a8a17b7d 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -1,5 +1,12 @@ { "domain": "amazon", "name": "Amazon", - "integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"] + "integrations": [ + "alexa", + "amazon_polly", + "aws", + "aws_s3", + "fire_tv", + "route53" + ] } diff --git a/homeassistant/components/s3/__init__.py b/homeassistant/components/aws_s3/__init__.py similarity index 98% rename from homeassistant/components/s3/__init__.py rename to homeassistant/components/aws_s3/__init__.py index 95e5e7d738c..b709595ae4a 100644 --- a/homeassistant/components/s3/__init__.py +++ b/homeassistant/components/aws_s3/__init__.py @@ -1,4 +1,4 @@ -"""The S3 integration.""" +"""The AWS S3 integration.""" from __future__ import annotations diff --git a/homeassistant/components/s3/backup.py b/homeassistant/components/aws_s3/backup.py similarity index 99% rename from homeassistant/components/s3/backup.py rename to homeassistant/components/aws_s3/backup.py index a58947d4c2d..7ef1289132d 100644 --- a/homeassistant/components/s3/backup.py +++ b/homeassistant/components/aws_s3/backup.py @@ -1,4 +1,4 @@ -"""Backup platform for the S3 integration.""" +"""Backup platform for the AWS S3 integration.""" from collections.abc import AsyncIterator, Callable, Coroutine import functools diff --git a/homeassistant/components/s3/config_flow.py b/homeassistant/components/aws_s3/config_flow.py similarity index 98% rename from homeassistant/components/s3/config_flow.py rename to homeassistant/components/aws_s3/config_flow.py index d721594b7bd..81ddd881f0f 100644 --- a/homeassistant/components/s3/config_flow.py +++ b/homeassistant/components/aws_s3/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for the S3 integration.""" +"""Config flow for the AWS S3 integration.""" from __future__ import annotations diff --git a/homeassistant/components/s3/const.py b/homeassistant/components/aws_s3/const.py similarity index 90% rename from homeassistant/components/s3/const.py rename to homeassistant/components/aws_s3/const.py index d992a92ac20..95d53c93a08 100644 --- a/homeassistant/components/s3/const.py +++ b/homeassistant/components/aws_s3/const.py @@ -1,11 +1,11 @@ -"""Constants for the S3 integration.""" +"""Constants for the AWS S3 integration.""" from collections.abc import Callable from typing import Final from homeassistant.util.hass_dict import HassKey -DOMAIN: Final = "s3" +DOMAIN: Final = "aws_s3" CONF_ACCESS_KEY_ID = "access_key_id" CONF_SECRET_ACCESS_KEY = "secret_access_key" diff --git a/homeassistant/components/s3/manifest.json b/homeassistant/components/aws_s3/manifest.json similarity index 66% rename from homeassistant/components/s3/manifest.json rename to homeassistant/components/aws_s3/manifest.json index 6a3026ff76d..8ab65b5883a 100644 --- a/homeassistant/components/s3/manifest.json +++ b/homeassistant/components/aws_s3/manifest.json @@ -1,9 +1,9 @@ { - "domain": "s3", - "name": "S3", + "domain": "aws_s3", + "name": "AWS S3", "codeowners": ["@tomasbedrich"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/s3", + "documentation": "https://www.home-assistant.io/integrations/aws_s3", "integration_type": "service", "iot_class": "cloud_push", "loggers": ["aiobotocore"], diff --git a/homeassistant/components/s3/quality_scale.yaml b/homeassistant/components/aws_s3/quality_scale.yaml similarity index 100% rename from homeassistant/components/s3/quality_scale.yaml rename to homeassistant/components/aws_s3/quality_scale.yaml diff --git a/homeassistant/components/s3/strings.json b/homeassistant/components/aws_s3/strings.json similarity index 73% rename from homeassistant/components/s3/strings.json rename to homeassistant/components/aws_s3/strings.json index 3404321be03..b5683aafa6e 100644 --- a/homeassistant/components/s3/strings.json +++ b/homeassistant/components/aws_s3/strings.json @@ -9,18 +9,18 @@ "endpoint_url": "Endpoint URL" }, "data_description": { - "access_key_id": "Access key ID to connect to S3 API", - "secret_access_key": "Secret access key to connect to S3 API", + "access_key_id": "Access key ID to connect to AWS S3 API", + "secret_access_key": "Secret access key to connect to AWS S3 API", "bucket": "Bucket must already exist and be writable by the provided credentials.", "endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs." }, - "title": "Add S3 bucket" + "title": "Add AWS S3 bucket" } }, "error": { - "cannot_connect": "[%key:component::s3::exceptions::cannot_connect::message%]", - "invalid_bucket_name": "[%key:component::s3::exceptions::invalid_bucket_name::message%]", - "invalid_credentials": "[%key:component::s3::exceptions::invalid_credentials::message%]", + "cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]", + "invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]", + "invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]", "invalid_endpoint_url": "Invalid endpoint URL" }, "abort": { diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8174dfc60b1..d3fae81d287 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -75,6 +75,7 @@ FLOWS = { "aussie_broadband", "autarco", "awair", + "aws_s3", "axis", "azure_data_explorer", "azure_devops", @@ -541,7 +542,6 @@ FLOWS = { "ruuvi_gateway", "ruuvitag_ble", "rympro", - "s3", "sabnzbd", "samsungtv", "sanix", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5e97e4c6626..d05944ce628 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -219,6 +219,12 @@ "iot_class": "cloud_push", "name": "Amazon Web Services (AWS)" }, + "aws_s3": { + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_push", + "name": "AWS S3" + }, "fire_tv": { "integration_type": "virtual", "config_flow": false, @@ -5622,12 +5628,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "s3": { - "name": "S3", - "integration_type": "service", - "config_flow": true, - "iot_class": "cloud_push" - }, "sabnzbd": { "name": "SABnzbd", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index f0e7f3b2464..33b4c390c75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -210,7 +210,7 @@ aioazuredevops==2.2.1 aiobafi6==0.9.0 # homeassistant.components.aws -# homeassistant.components.s3 +# homeassistant.components.aws_s3 aiobotocore==2.21.1 # homeassistant.components.comelit diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc8f334698a..9c382578d10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aioazuredevops==2.2.1 aiobafi6==0.9.0 # homeassistant.components.aws -# homeassistant.components.s3 +# homeassistant.components.aws_s3 aiobotocore==2.21.1 # homeassistant.components.comelit diff --git a/tests/components/s3/__init__.py b/tests/components/aws_s3/__init__.py similarity index 90% rename from tests/components/s3/__init__.py rename to tests/components/aws_s3/__init__.py index 570747e69d0..90e4652bb2b 100644 --- a/tests/components/s3/__init__.py +++ b/tests/components/aws_s3/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the S3 integration.""" +"""Tests for the AWS S3 integration.""" from homeassistant.core import HomeAssistant diff --git a/tests/components/s3/conftest.py b/tests/components/aws_s3/conftest.py similarity index 93% rename from tests/components/s3/conftest.py rename to tests/components/aws_s3/conftest.py index a2c2b9eb3dd..8f12ee17661 100644 --- a/tests/components/s3/conftest.py +++ b/tests/components/aws_s3/conftest.py @@ -1,4 +1,4 @@ -"""Common fixtures for the S3 tests.""" +"""Common fixtures for the AWS S3 tests.""" from collections.abc import AsyncIterator, Generator import json @@ -6,12 +6,12 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components.backup import AgentBackup -from homeassistant.components.s3.backup import ( +from homeassistant.components.aws_s3.backup import ( MULTIPART_MIN_PART_SIZE_BYTES, suggested_filenames, ) -from homeassistant.components.s3.const import DOMAIN +from homeassistant.components.aws_s3.const import DOMAIN +from homeassistant.components.backup import AgentBackup from .const import USER_INPUT diff --git a/tests/components/s3/const.py b/tests/components/aws_s3/const.py similarity index 78% rename from tests/components/s3/const.py rename to tests/components/aws_s3/const.py index 92ebc080f2c..443275d0444 100644 --- a/tests/components/s3/const.py +++ b/tests/components/aws_s3/const.py @@ -1,6 +1,6 @@ -"""Consts for S3 tests.""" +"""Consts for AWS S3 tests.""" -from homeassistant.components.s3.const import ( +from homeassistant.components.aws_s3.const import ( CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_ENDPOINT_URL, diff --git a/tests/components/s3/test_backup.py b/tests/components/aws_s3/test_backup.py similarity index 98% rename from tests/components/s3/test_backup.py rename to tests/components/aws_s3/test_backup.py index 535e546dd21..a8b24ec1ab4 100644 --- a/tests/components/s3/test_backup.py +++ b/tests/components/aws_s3/test_backup.py @@ -1,4 +1,4 @@ -"""Test the S3 backup platform.""" +"""Test the AWS S3 backup platform.""" from collections.abc import AsyncGenerator from io import StringIO @@ -9,19 +9,19 @@ from unittest.mock import AsyncMock, Mock, patch from botocore.exceptions import ConnectTimeoutError import pytest -from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup -from homeassistant.components.s3.backup import ( +from homeassistant.components.aws_s3.backup import ( MULTIPART_MIN_PART_SIZE_BYTES, BotoCoreError, S3BackupAgent, async_register_backup_agents_listener, suggested_filenames, ) -from homeassistant.components.s3.const import ( +from homeassistant.components.aws_s3.const import ( CONF_ENDPOINT_URL, DATA_BACKUP_AGENT_LISTENERS, DOMAIN, ) +from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup from homeassistant.core import HomeAssistant from homeassistant.helpers.backup import async_initialize_backup from homeassistant.setup import async_setup_component @@ -362,7 +362,7 @@ async def test_agents_upload_network_failure( ) assert resp.status == 201 - assert "Upload failed for s3" in caplog.text + assert "Upload failed for aws_s3" in caplog.text async def test_agents_download( diff --git a/tests/components/s3/test_config_flow.py b/tests/components/aws_s3/test_config_flow.py similarity index 96% rename from tests/components/s3/test_config_flow.py rename to tests/components/aws_s3/test_config_flow.py index 1ea59a3aeb5..061d990140a 100644 --- a/tests/components/s3/test_config_flow.py +++ b/tests/components/aws_s3/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the S3 config flow.""" +"""Test the AWS S3 config flow.""" from unittest.mock import AsyncMock, patch @@ -10,7 +10,7 @@ from botocore.exceptions import ( import pytest from homeassistant import config_entries -from homeassistant.components.s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN +from homeassistant.components.aws_s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/s3/test_init.py b/tests/components/aws_s3/test_init.py similarity index 98% rename from tests/components/s3/test_init.py rename to tests/components/aws_s3/test_init.py index afa11f5cf72..ee247bfce1d 100644 --- a/tests/components/s3/test_init.py +++ b/tests/components/aws_s3/test_init.py @@ -1,4 +1,4 @@ -"""Test the s3 storage integration.""" +"""Test the AWS S3 storage integration.""" from unittest.mock import AsyncMock, patch From 4b7c337dc9be8015404c9a5fb49d825101cf1fd3 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 6 May 2025 14:49:47 +0200 Subject: [PATCH 080/110] Update Home Assistant base image to 2025.05.0 (#144333) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 87dad1bf5ef..00df4196523 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 9150c78901fb9a65e8ea443f9ac0e31de174c611 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 May 2025 15:54:42 +0200 Subject: [PATCH 081/110] Add endpoint validation for AWS S3 (#144334) --- .../components/aws_s3/config_flow.py | 48 +++++++++++-------- homeassistant/components/aws_s3/const.py | 3 +- homeassistant/components/aws_s3/strings.json | 2 +- tests/components/aws_s3/const.py | 2 +- tests/components/aws_s3/test_config_flow.py | 27 ++++++++++- 5 files changed, 58 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/aws_s3/config_flow.py b/homeassistant/components/aws_s3/config_flow.py index 81ddd881f0f..a4de192e513 100644 --- a/homeassistant/components/aws_s3/config_flow.py +++ b/homeassistant/components/aws_s3/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from urllib.parse import urlparse from aiobotocore.session import AioSession from botocore.exceptions import ClientError, ConnectionError, ParamValidationError @@ -17,6 +18,7 @@ from homeassistant.helpers.selector import ( ) from .const import ( + AWS_DOMAIN, CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_ENDPOINT_URL, @@ -57,28 +59,34 @@ class S3ConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL], } ) - try: - session = AioSession() - async with session.create_client( - "s3", - endpoint_url=user_input.get(CONF_ENDPOINT_URL), - aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY], - aws_access_key_id=user_input[CONF_ACCESS_KEY_ID], - ) as client: - await client.head_bucket(Bucket=user_input[CONF_BUCKET]) - except ClientError: - errors["base"] = "invalid_credentials" - except ParamValidationError as err: - if "Invalid bucket name" in str(err): - errors[CONF_BUCKET] = "invalid_bucket_name" - except ValueError: + + if not urlparse(user_input[CONF_ENDPOINT_URL]).hostname.endswith( + AWS_DOMAIN + ): errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url" - except ConnectionError: - errors[CONF_ENDPOINT_URL] = "cannot_connect" else: - return self.async_create_entry( - title=user_input[CONF_BUCKET], data=user_input - ) + try: + session = AioSession() + async with session.create_client( + "s3", + endpoint_url=user_input.get(CONF_ENDPOINT_URL), + aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY], + aws_access_key_id=user_input[CONF_ACCESS_KEY_ID], + ) as client: + await client.head_bucket(Bucket=user_input[CONF_BUCKET]) + except ClientError: + errors["base"] = "invalid_credentials" + except ParamValidationError as err: + if "Invalid bucket name" in str(err): + errors[CONF_BUCKET] = "invalid_bucket_name" + except ValueError: + errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url" + except ConnectionError: + errors[CONF_ENDPOINT_URL] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_BUCKET], data=user_input + ) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/aws_s3/const.py b/homeassistant/components/aws_s3/const.py index 95d53c93a08..a6863e6c38a 100644 --- a/homeassistant/components/aws_s3/const.py +++ b/homeassistant/components/aws_s3/const.py @@ -12,7 +12,8 @@ CONF_SECRET_ACCESS_KEY = "secret_access_key" CONF_ENDPOINT_URL = "endpoint_url" CONF_BUCKET = "bucket" -DEFAULT_ENDPOINT_URL = "https://s3.eu-central-1.amazonaws.com/" +AWS_DOMAIN = "amazonaws.com" +DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/" DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey( f"{DOMAIN}.backup_agent_listeners" diff --git a/homeassistant/components/aws_s3/strings.json b/homeassistant/components/aws_s3/strings.json index b5683aafa6e..84a7f68c850 100644 --- a/homeassistant/components/aws_s3/strings.json +++ b/homeassistant/components/aws_s3/strings.json @@ -21,7 +21,7 @@ "cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]", "invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]", "invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]", - "invalid_endpoint_url": "Invalid endpoint URL" + "invalid_endpoint_url": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/tests/components/aws_s3/const.py b/tests/components/aws_s3/const.py index 443275d0444..ebffa11d956 100644 --- a/tests/components/aws_s3/const.py +++ b/tests/components/aws_s3/const.py @@ -10,6 +10,6 @@ from homeassistant.components.aws_s3.const import ( USER_INPUT = { CONF_ACCESS_KEY_ID: "TestTestTestTestTest", CONF_SECRET_ACCESS_KEY: "TestTestTestTestTestTestTestTestTestTest", - CONF_ENDPOINT_URL: "http://127.0.0.1:9000", + CONF_ENDPOINT_URL: "https://s3.eu-south-1.amazonaws.com", CONF_BUCKET: "test", } diff --git a/tests/components/aws_s3/test_config_flow.py b/tests/components/aws_s3/test_config_flow.py index 061d990140a..593eea5cdb9 100644 --- a/tests/components/aws_s3/test_config_flow.py +++ b/tests/components/aws_s3/test_config_flow.py @@ -21,8 +21,12 @@ from tests.common import MockConfigEntry async def _async_start_flow( hass: HomeAssistant, + user_input: dict[str, str] | None = None, ) -> FlowResultType: """Initialize the config flow.""" + if user_input is None: + user_input = USER_INPUT + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -30,7 +34,7 @@ async def _async_start_flow( return await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + user_input, ) @@ -116,3 +120,24 @@ async def test_abort_if_already_configured( result = await _async_start_flow(hass) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_flow_create_not_aws_endpoint( + hass: HomeAssistant, +) -> None: + """Test config flow with a not aws endpoint should raise an error.""" + result = await _async_start_flow( + hass, USER_INPUT | {CONF_ENDPOINT_URL: "http://example.com"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {CONF_ENDPOINT_URL: "invalid_endpoint_url"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test" + assert result["data"] == USER_INPUT From 7cc142dd59b27894ef629e96fb320a5e93cb069e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 May 2025 15:26:45 +0200 Subject: [PATCH 082/110] Fix Z-Wave to reload config entry after migration nvm restore (#144338) --- .../components/zwave_js/config_flow.py | 17 +- tests/components/zwave_js/test_config_flow.py | 301 ++++++++++++++++++ 2 files changed, 317 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index c6624046a00..46d9e061f0b 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from datetime import datetime import logging from pathlib import Path @@ -77,6 +78,7 @@ ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" +RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 ADDON_LOG_LEVELS = { @@ -1317,15 +1319,28 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): event["bytesWritten"] / event["total"] * 0.5 + 0.5 ) - controller = self._get_driver().controller + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + driver = self._get_driver() + controller = driver.controller + wait_driver_ready = asyncio.Event() unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), + driver.once("driver ready", set_driver_ready), ] try: await controller.async_restore_nvm(self.backup_data) except FailedCommand as err: raise AbortFlow(f"Failed to restore network: {err}") from err + else: + with suppress(TimeoutError): + async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + await self.hass.config_entries.async_reload(config_entry.entry_id) finally: for unsub in unsubs: unsub() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 08f0ffad4bd..de76d9d9dc4 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -190,6 +190,19 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version +@pytest.fixture(name="driver_ready_timeout") +def mock_driver_ready_timeout() -> Generator[None]: + """Mock migration nvm restore driver ready timeout.""" + with patch( + ( + "homeassistant.components.zwave_js.config_flow." + "RESTORE_NVM_DRIVER_READY_TIMEOUT" + ), + new=0, + ): + yield + + async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -889,6 +902,144 @@ async def test_usb_discovery_migration( """Test usb discovery migration.""" addon_options["device"] = "/dev/ttyUSB0" entry = integration + assert client.connect.call_count == 1 + hass.config_entries.async_update_entry( + entry, + unique_id="1234", + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USB}, + data=USB_DISCOVERY_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", AddonsOptions(config={"device": USB_DISCOVERY_INFO.device}) + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert integration.data["url"] == "ws://host1:3001" + assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device + assert integration.data["use_addon"] is True + + +@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_usb_discovery_migration_driver_ready_timeout( + hass: HomeAssistant, + addon_options: dict[str, Any], + driver_ready_timeout: None, + mock_usb_serial_by_id: MagicMock, + set_addon_options: AsyncMock, + restart_addon: AsyncMock, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test driver ready timeout after nvm restore during usb discovery migration.""" + addon_options["device"] = "/dev/ttyUSB0" + entry = integration + assert client.connect.call_count == 1 hass.config_entries.async_update_entry( entry, unique_id="1234", @@ -976,8 +1127,10 @@ async def test_usb_discovery_migration( assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 await hass.async_block_till_done() + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3552,6 +3705,152 @@ async def test_reconfigure_migrate_with_addon( ) -> None: """Test migration flow with add-on.""" entry = integration + assert client.connect.call_count == 1 + hass.config_entries.async_update_entry( + entry, + unique_id="1234", + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + assert result["data_schema"].schema[CONF_USB_PATH] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", AddonsOptions(config={"device": "/test"}) + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert integration.data["url"] == "ws://host1:3001" + assert integration.data["usb_path"] == "/test" + assert integration.data["use_addon"] is True + + +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_reconfigure_migrate_driver_ready_timeout( + hass: HomeAssistant, + client, + supervisor, + integration, + addon_running, + driver_ready_timeout: None, + restart_addon, + set_addon_options, + get_addon_discovery_info, +) -> None: + """Test migration flow with driver ready timeout after nvm restore.""" + entry = integration + assert client.connect.call_count == 1 hass.config_entries.async_update_entry( entry, unique_id="1234", @@ -3648,8 +3947,10 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 await hass.async_block_till_done() + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 From 5ed3f18d706ac165b5331148f089092ab8d6381e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 May 2025 13:57:05 +0000 Subject: [PATCH 083/110] Bump version to 2025.5.0b6 --- 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 78761216104..f1b1a81c3dc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 7a63cdc61dd..7c5ff24c17d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b5" +version = "2025.5.0b6" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 806bcf47d95ecb0503a7986b5b97f73143bd8a74 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 May 2025 16:57:11 +0200 Subject: [PATCH 084/110] Fix Z-Wave migration flow to unload config entry before unplugging controller (#144343) * Fix Z-Wave migration unload config entry before unplugging controller * Remove typo --- homeassistant/components/zwave_js/config_flow.py | 9 +++++---- tests/components/zwave_js/test_config_flow.py | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 46d9e061f0b..84717047fdd 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -907,10 +907,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): """Reset the current controller, and instruct the user to unplug it.""" if user_input is not None: - config_entry = self._reconfigure_config_entry - assert config_entry is not None - # Unload the config entry before stopping the add-on. - await self.hass.config_entries.async_unload(config_entry.entry_id) if self.usb_path: # USB discovery was used, so the device is already known. await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) @@ -925,6 +921,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to reset controller: %s", err) return self.async_abort(reason="reset_failed") + config_entry = self._reconfigure_config_entry + assert config_entry is not None + # Unload the config entry before asking the user to unplug the controller. + await self.hass.config_entries.async_unload(config_entry.entry_id) + return self.async_show_form( step_id="instruct_unplug", description_placeholders={ diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index de76d9d9dc4..15fd9fcbd30 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1109,10 +1109,10 @@ async def test_usb_discovery_migration_driver_ready_timeout( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( @@ -3776,6 +3776,7 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3790,7 +3791,6 @@ async def test_reconfigure_migrate_with_addon( }, ) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( @@ -3918,6 +3918,7 @@ async def test_reconfigure_migrate_driver_ready_timeout( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3932,7 +3933,6 @@ async def test_reconfigure_migrate_driver_ready_timeout( }, ) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( @@ -4108,6 +4108,7 @@ async def test_reconfigure_migrate_start_addon_failure( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -4202,6 +4203,7 @@ async def test_reconfigure_migrate_restore_failure( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -4367,6 +4369,7 @@ async def test_choose_serial_port_usb_ports_failure( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.zwave_js.config_flow.async_get_usb_ports", From ccffe196117e63047bd2a4e760fab14cfcf176da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 May 2025 10:32:35 -0500 Subject: [PATCH 085/110] Bump bluemaestro-ble to 0.4.1 (#144345) changelog: https://github.com/Bluetooth-Devices/bluemaestro-ble/compare/v0.4.0...v0.4.1 fixes #https://github.com/home-assistant/core/issues/144339 --- homeassistant/components/bluemaestro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 5e3c43f4ff9..887b27239ef 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bluemaestro", "iot_class": "local_push", - "requirements": ["bluemaestro-ble==0.4.0"] + "requirements": ["bluemaestro-ble==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 33b4c390c75..b25829aa821 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ blockchain==1.4.4 bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro -bluemaestro-ble==0.4.0 +bluemaestro-ble==0.4.1 # homeassistant.components.decora # bluepy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c382578d10..14357246755 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ blinkpy==0.23.0 bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro -bluemaestro-ble==0.4.0 +bluemaestro-ble==0.4.1 # homeassistant.components.bluetooth bluetooth-adapters==0.21.4 From de63dddc960d9676cb883138f83c8f15d0d5641d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 May 2025 19:30:48 +0200 Subject: [PATCH 086/110] Ensure all default MQTT subentry option values are saved (#144347) * Ensure all default MQTT subentry option values are saved * Apply correct filter --- homeassistant/components/mqtt/config_flow.py | 12 +++++++++--- tests/components/mqtt/common.py | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 02c8a1cdc8a..e2acf7e88b8 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1385,8 +1385,11 @@ def subentry_schema_default_data_from_fields( return { key: field.default for key, field in data_schema_fields.items() - if field.is_schema_default - or (field.default is not vol.UNDEFINED and key not in component_data) + if _check_conditions(field, component_data) + and ( + field.is_schema_default + or (field.default is not vol.UNDEFINED and key not in component_data) + ) } @@ -2212,7 +2215,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): for component_data in self._subentry_data["components"].values(): platform = component_data[CONF_PLATFORM] subentry_default_data = subentry_schema_default_data_from_fields( - PLATFORM_ENTITY_FIELDS[platform] | COMMON_ENTITY_FIELDS, component_data + COMMON_ENTITY_FIELDS + | PLATFORM_ENTITY_FIELDS[platform] + | PLATFORM_MQTT_FIELDS[platform], + component_data, ) component_data.update(subentry_default_data) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 4e402046e2c..3e920757f6b 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -153,6 +153,10 @@ MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT = { "state_topic": "test-topic", "color_temp_kelvin": True, "state_value_template": "{{ value_json.value }}", + "brightness_scale": 255, + "max_kelvin": 6535, + "min_kelvin": 2000, + "white_scale": 255, "entity_picture": "https://example.com/8131babc5e8d4f44b82e0761d39091a2", }, } From d16453a4657e75e2c8a735509311c3f76f28a0cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 May 2025 15:24:32 -0400 Subject: [PATCH 087/110] Remove some media player intent checks for when paused (#144351) --- .../components/media_player/intent.py | 2 -- tests/components/media_player/test_intent.py | 24 ------------------- 2 files changed, 26 deletions(-) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index af37c0d68bb..4349362b13a 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -93,7 +93,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None: DOMAIN, SERVICE_VOLUME_SET, required_domains={DOMAIN}, - required_states={MediaPlayerState.PLAYING}, required_features=MediaPlayerEntityFeature.VOLUME_SET, required_slots={ ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo( @@ -159,7 +158,6 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler): DOMAIN, SERVICE_MEDIA_PLAY, required_domains={DOMAIN}, - required_states={MediaPlayerState.PAUSED}, description="Resumes a media player", platforms={DOMAIN}, device_classes={MediaPlayerDeviceClass}, diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index 9ddf50d04f4..8e7211183e7 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -104,19 +104,6 @@ async def test_unpause_media_player_intent(hass: HomeAssistant) -> None: assert call.service == SERVICE_MEDIA_PLAY assert call.data == {"entity_id": entity_id} - # Test if not paused - hass.states.async_set( - entity_id, - STATE_PLAYING, - ) - - with pytest.raises(intent.MatchFailedError): - response = await intent.async_handle( - hass, - "test", - media_player_intent.INTENT_MEDIA_UNPAUSE, - ) - async def test_next_media_player_intent(hass: HomeAssistant) -> None: """Test HassMediaNext intent for media players.""" @@ -245,17 +232,6 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None: assert call.service == SERVICE_VOLUME_SET assert call.data == {"entity_id": entity_id, "volume_level": 0.5} - # Test if not playing - hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes) - - with pytest.raises(intent.MatchFailedError): - response = await intent.async_handle( - hass, - "test", - media_player_intent.INTENT_SET_VOLUME, - {"volume_level": {"value": 50}}, - ) - # Test feature not supported hass.states.async_set( entity_id, From 2a3bd4590111af4461f96b67125f27a9008d27c9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 May 2025 21:24:09 +0200 Subject: [PATCH 088/110] Update frontend to 20250506.0 (#144354) --- 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 18e4d349122..4abf9aa7814 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==20250502.1"] + "requirements": ["home-assistant-frontend==20250506.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a9788e03648..1838e552800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250502.1 +home-assistant-frontend==20250506.0 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b25829aa821..12563a3c9cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.1 +home-assistant-frontend==20250506.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14357246755..bd7a0d40d00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.1 +home-assistant-frontend==20250506.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From 1eeab28eec039f7927846ff7e97081a44fd4acd5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 May 2025 19:30:08 +0000 Subject: [PATCH 089/110] Bump version to 2025.5.0b7 --- 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 f1b1a81c3dc..cd5800886f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 7c5ff24c17d..751030c1de5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b6" +version = "2025.5.0b7" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From e217532f9ee801b6ac80b4205e549e34be0e820e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 7 May 2025 09:24:51 +0200 Subject: [PATCH 090/110] Fix field validation for mqtt subentry options in sections (#144355) --- homeassistant/components/mqtt/config_flow.py | 8 +++++--- tests/components/mqtt/test_config_flow.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index e2acf7e88b8..74f55afabaa 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -498,8 +498,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get( CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN ): - errors[CONF_MAX_KELVIN] = "max_below_min_kelvin" - errors[CONF_MIN_KELVIN] = "max_below_min_kelvin" + errors["advanced_settings"] = "max_below_min_kelvin" return errors @@ -1276,7 +1275,10 @@ def validate_user_input( try: validator(value) except (ValueError, vol.Error, vol.Invalid): - errors[field] = data_schema_fields[field].error or "invalid_input" + data_schema_field = data_schema_fields[field] + errors[data_schema_field.section or field] = ( + data_schema_field.error or "invalid_input" + ) if config_validator is not None: if TYPE_CHECKING: diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index b3d2769de6a..11f5b9d5c9e 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -2817,14 +2817,22 @@ async def test_migrate_of_incompatible_config_entry( }, {"state_topic": "invalid_subscribe_topic"}, ), + ( + { + "command_topic": "test-topic", + "light_brightness_settings": { + "brightness_command_topic": "test-topic#invalid" + }, + }, + {"light_brightness_settings": "invalid_publish_topic"}, + ), ( { "command_topic": "test-topic", "advanced_settings": {"max_kelvin": 2000, "min_kelvin": 2000}, }, { - "max_kelvin": "max_below_min_kelvin", - "min_kelvin": "max_below_min_kelvin", + "advanced_settings": "max_below_min_kelvin", }, ), ), From 983e134ae97d4fec5c2d6079136327f86ad85439 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 08:44:55 +0200 Subject: [PATCH 091/110] Bump renault-api to 0.3.1 (#144366) * Bump renault-api to 0.3.1 * Adjust tests --- .../components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../renault/snapshots/test_binary_sensor.ambr | 576 ------------------ .../renault/snapshots/test_sensor.ambr | 188 ------ tests/components/renault/test_sensor.py | 4 +- 6 files changed, 5 insertions(+), 769 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 06acf4a3e49..2861c52c24a 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "silver", - "requirements": ["renault-api==0.3.0"] + "requirements": ["renault-api==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 12563a3c9cc..d90d3c3c47d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2631,7 +2631,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.0 +renault-api==0.3.1 # homeassistant.components.renson renson-endura-delta==1.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd7a0d40d00..5061585232e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2138,7 +2138,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.0 +renault-api==0.3.1 # homeassistant.components.renson renson-endura-delta==1.7.2 diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index d1547bc1bbc..e89873593e9 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -1005,102 +1005,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_driver_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Driver door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'driver_door_status', - 'unique_id': 'vf1twingoiiivin_driver_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Driver door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_driver_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_hatch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hatch', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hatch_status', - 'unique_id': 'vf1twingoiiivin_hatch_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Hatch', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_hatch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hvac-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1148,102 +1052,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Lock', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'vf1twingoiiivin_lock_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'lock', - 'friendly_name': 'REG-TWINGO-III Lock', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Passenger door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'passenger_door_status', - 'unique_id': 'vf1twingoiiivin_passenger_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Passenger door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_plug-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1292,102 +1100,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear left door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_left_door_status', - 'unique_id': 'vf1twingoiiivin_rear_left_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Rear left door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear right door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_right_door_status', - 'unique_id': 'vf1twingoiiivin_rear_right_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Rear right door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_binary_sensors[zoe_40][binary_sensor.reg_zoe_40_charging-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1579,102 +1291,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_driver_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Driver door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'driver_door_status', - 'unique_id': 'vf1zoe50vin_driver_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Driver door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_driver_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_hatch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hatch', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hatch_status', - 'unique_id': 'vf1zoe50vin_hatch_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Hatch', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_hatch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hvac-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1722,102 +1338,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Lock', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'vf1zoe50vin_lock_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'lock', - 'friendly_name': 'REG-ZOE-50 Lock', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_passenger_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Passenger door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'passenger_door_status', - 'unique_id': 'vf1zoe50vin_passenger_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Passenger door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_passenger_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_plug-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1866,99 +1386,3 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear left door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_left_door_status', - 'unique_id': 'vf1zoe50vin_rear_left_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Rear left door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear right door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_right_door_status', - 'unique_id': 'vf1zoe50vin_rear_right_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Rear right door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index e7300d2b003..b6c9569e0d3 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -3211,100 +3211,6 @@ 'state': 'unknown', }) # --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state', - 'unique_id': 'vf1twingoiiivin_res_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-TWINGO-III Remote engine start', - }), - 'context': , - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start code', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state_code', - 'unique_id': 'vf1twingoiiivin_res_state_code', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-TWINGO-III Remote engine start code', - }), - 'context': , - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensors[zoe_40][sensor.reg_zoe_40_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4737,97 +4643,3 @@ 'state': 'unplugged', }) # --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state', - 'unique_id': 'vf1zoe50vin_res_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-ZOE-50 Remote engine start', - }), - 'context': , - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Stopped, ready for RES', - }) -# --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start code', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state_code', - 'unique_id': 'vf1zoe50vin_res_state_code', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-ZOE-50 Remote engine start code', - }), - 'context': , - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10', - }) -# --- diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 10fa2f0ffb0..e75d0558f19 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -197,7 +197,7 @@ async def test_sensor_throttling_after_init( @pytest.mark.parametrize( ("vehicle_type", "vehicle_count", "scan_interval"), [ - ("zoe_50", 1, 420), # 7 coordinators => 7 minutes interval + ("zoe_50", 1, 300), # 5 coordinators => 5 minutes interval ("captur_fuel", 1, 240), # 4 coordinators => 4 minutes interval ("multi", 2, 480), # 8 coordinators => 8 minutes interval ], @@ -236,7 +236,7 @@ async def test_dynamic_scan_interval( @pytest.mark.parametrize( ("vehicle_type", "vehicle_count", "scan_interval"), [ - ("zoe_50", 1, 300), # (7-2) coordinators => 5 minutes interval + ("zoe_50", 1, 240), # (6-2) coordinators => 4 minutes interval ("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval ("multi", 2, 360), # (8-2) coordinators => 6 minutes interval ], From a9632bd0ff948c09dec927f9ccd7cf190e277717 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Wed, 7 May 2025 09:11:31 +0200 Subject: [PATCH 092/110] Bump uiprotect to version 7.6.0 (#144369) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a3f3b6fe2eb..e23568480ca 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.5.5", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.6.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index d90d3c3c47d..e4677c6ba2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2975,7 +2975,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.5.5 +uiprotect==7.6.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5061585232e..19f5b894f4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2404,7 +2404,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.5.5 +uiprotect==7.6.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 35c90d9bdeec5d6526df38882ddb4f3cdcdc0d03 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 May 2025 07:38:18 +0000 Subject: [PATCH 093/110] Bump version to 2025.5.0b8 --- 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 cd5800886f8..224a3eaee21 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 751030c1de5..c81b7c1616f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b7" +version = "2025.5.0b8" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 07b2ce28b19d87f5ff674b853621b885c6a32f28 Mon Sep 17 00:00:00 2001 From: "Barry vd. Heuvel" Date: Wed, 7 May 2025 13:04:46 +0200 Subject: [PATCH 094/110] Bump wh-python to 2025.4.29 for Weheat integration (#144384) --- homeassistant/components/weheat/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weheat/manifest.json b/homeassistant/components/weheat/manifest.json index 3a4cff6f295..cd631866fdb 100644 --- a/homeassistant/components/weheat/manifest.json +++ b/homeassistant/components/weheat/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/weheat", "iot_class": "cloud_polling", - "requirements": ["weheat==2025.3.7"] + "requirements": ["weheat==2025.4.29"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4677c6ba2c..0d0380dd1eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3074,7 +3074,7 @@ webio-api==0.1.11 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2025.3.7 +weheat==2025.4.29 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.20.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19f5b894f4c..ece75eec2ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2485,7 +2485,7 @@ webio-api==0.1.11 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2025.3.7 +weheat==2025.4.29 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.20.0 From d2e7baeb38b950cda0989c2dd47aa6e45218de2e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 7 May 2025 12:32:27 +0200 Subject: [PATCH 095/110] Fix Z-Wave controller hard reset (#144389) --- homeassistant/components/zwave_js/api.py | 22 +++++- tests/components/zwave_js/test_api.py | 94 +++++++++++++++++------- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index eb86a344c6e..aa2219031d2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,9 @@ from __future__ import annotations +import asyncio from collections.abc import Callable, Coroutine +from contextlib import suppress import dataclasses from functools import partial, wraps from typing import Any, Concatenate, Literal, cast @@ -182,6 +184,8 @@ STRATEGY = "strategy" # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41 MINIMUM_QR_STRING_LENGTH = 52 +HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT = 60 + # Helper schemas PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( @@ -2816,6 +2820,7 @@ async def websocket_hard_reset_controller( driver: Driver, ) -> None: """Hard reset controller.""" + unsubs: list[Callable[[], None]] @callback def async_cleanup() -> None: @@ -2831,13 +2836,28 @@ async def websocket_hard_reset_controller( connection.send_result(msg[ID], device.id) async_cleanup() + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + msg[DATA_UNSUBSCRIBE] = unsubs = [ async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added - ) + ), + driver.once("driver ready", set_driver_ready), ] + await driver.async_hard_reset() + with suppress(TimeoutError): + async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + + await hass.config_entries.async_reload(entry.entry_id) + @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index c63283fd220..2e3d8fd290a 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5,7 +5,7 @@ from http import HTTPStatus from io import BytesIO import json from typing import Any -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch import pytest from zwave_js_server.const import ( @@ -5078,53 +5078,97 @@ async def test_subscribe_node_statistics( assert msg["error"]["code"] == ERR_NOT_LOADED -@pytest.mark.skip( - reason="The test needs to be updated to reflect what happens when resetting the controller" -) async def test_hard_reset_controller( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - client, - integration, - listen_block, + client: MagicMock, + integration: MockConfigEntry, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the hard_reset_controller WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) - device = device_registry.async_get_device( - identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} - ) + async def async_send_command_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + return {} - client.async_send_command.return_value = {} - await ws_client.send_json( + client.async_send_command.side_effect = async_send_command_driver_ready + + await ws_client.send_json_auto_id( { - ID: 1, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: entry.entry_id, } ) - - listen_block.set() - listen_block.clear() - await hass.async_block_till_done() - msg = await ws_client.receive_json() + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device is not None assert msg["result"] == device.id assert msg["success"] - assert len(client.async_send_command.call_args_list) == 1 - assert client.async_send_command.call_args[0][0] == {"command": "driver.hard_reset"} + assert client.async_send_command.call_count == 3 + # The first call is the relevant hard reset command. + # 25 is the require_schema parameter. + assert client.async_send_command.call_args_list[0] == call( + {"command": "driver.hard_reset"}, 25 + ) + + client.async_send_command.reset_mock() + + # Test sending command with driver not ready and timeout. + + async def async_send_command_no_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + return {} + + client.async_send_command.side_effect = async_send_command_no_driver_ready + + with patch( + "homeassistant.components.zwave_js.api.HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT", + new=0, + ): + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device is not None + assert msg["result"] == device.id + assert msg["success"] + + assert client.async_send_command.call_count == 3 + # The first call is the relevant hard reset command. + # 25 is the require_schema parameter. + assert client.async_send_command.call_args_list[0] == call( + {"command": "driver.hard_reset"}, 25 + ) # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.driver.Driver.async_hard_reset", side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): - await ws_client.send_json( + await ws_client.send_json_auto_id( { - ID: 2, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: entry.entry_id, } @@ -5139,9 +5183,8 @@ async def test_hard_reset_controller( await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - await ws_client.send_json( + await ws_client.send_json_auto_id( { - ID: 3, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: entry.entry_id, } @@ -5151,9 +5194,8 @@ async def test_hard_reset_controller( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED - await ws_client.send_json( + await ws_client.send_json_auto_id( { - ID: 4, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: "INVALID", } From 85a83f25535be4b072a3aa82a1e93f4446f797a1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 12:37:53 +0200 Subject: [PATCH 096/110] Fix SmartThings machine operating state with no options (#144390) --- .../components/smartthings/select.py | 11 +- tests/components/smartthings/conftest.py | 1 + .../device_status/da_wm_wm_100001.json | 154 ++++++++++++++ .../fixtures/devices/da_wm_wm_100001.json | 84 ++++++++ .../snapshots/test_binary_sensor.ambr | 95 +++++++++ .../smartthings/snapshots/test_init.ambr | 33 +++ .../smartthings/snapshots/test_select.ambr | 58 ++++++ .../smartthings/snapshots/test_sensor.ambr | 192 ++++++++++++++++++ 8 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json create mode 100644 tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 63dcb90b019..16051cb08f1 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -26,6 +26,7 @@ class SmartThingsSelectDescription(SelectEntityDescription): options_attribute: Attribute status_attribute: Attribute command: Command + default_options: list[str] | None = None CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { @@ -46,6 +47,7 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { options_attribute=Attribute.SUPPORTED_MACHINE_STATES, status_attribute=Attribute.MACHINE_STATE, command=Command.SET_MACHINE_STATE, + default_options=["run", "pause", "stop"], ), Capability.WASHER_OPERATING_STATE: SmartThingsSelectDescription( key=Capability.WASHER_OPERATING_STATE, @@ -55,6 +57,7 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { options_attribute=Attribute.SUPPORTED_MACHINE_STATES, status_attribute=Attribute.MACHINE_STATE, command=Command.SET_MACHINE_STATE, + default_options=["run", "pause", "stop"], ), Capability.SAMSUNG_CE_AUTO_DISPENSE_DETERGENT: SmartThingsSelectDescription( key=Capability.SAMSUNG_CE_AUTO_DISPENSE_DETERGENT, @@ -114,8 +117,12 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity): @property def options(self) -> list[str]: """Return the list of options.""" - return self.get_attribute_value( - self.entity_description.key, self.entity_description.options_attribute + return ( + self.get_attribute_value( + self.entity_description.key, self.entity_description.options_attribute + ) + or self.entity_description.default_options + or [] ) @property diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 244b89ca06a..b3a58b17637 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -122,6 +122,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "da_wm_wd_000001", "da_wm_wd_000001_1", "da_wm_wm_01011", + "da_wm_wm_100001", "da_wm_wm_000001", "da_wm_wm_000001_1", "da_wm_sc_000001", diff --git a/tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json b/tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json new file mode 100644 index 00000000000..b3b01762099 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json @@ -0,0 +1,154 @@ +{ + "components": { + "main": { + "ocf": { + "st": { + "value": null, + "timestamp": "2020-10-06T23:01:03.011Z" + }, + "mndt": { + "value": null, + "timestamp": "2021-01-28T11:54:37.203Z" + }, + "mnfv": { + "value": null, + "timestamp": "2020-12-20T14:21:43.766Z" + }, + "mnhw": { + "value": null, + "timestamp": "2021-01-25T22:57:01.985Z" + }, + "di": { + "value": "C0972771-01D0-0000-0000-000000000000", + "timestamp": "2019-08-10T18:37:20.487Z" + }, + "mnsl": { + "value": null, + "timestamp": "2020-12-20T14:21:31.219Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2019-08-10T18:37:20.514Z" + }, + "n": { + "value": "Washer", + "timestamp": "2019-08-10T18:37:20.555Z" + }, + "mnmo": { + "value": "TP6X_WA54M8750AV|20183944|20000101001111000100000000000000", + "timestamp": "2019-08-10T18:37:20.409Z" + }, + "vid": { + "value": "DA-WM-WM-100001", + "timestamp": "2019-08-10T18:37:20.381Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2019-08-10T18:37:20.436Z" + }, + "mnml": { + "value": null, + "timestamp": "2021-01-28T11:54:37.092Z" + }, + "mnpv": { + "value": null, + "timestamp": "2021-01-26T20:55:28.663Z" + }, + "mnos": { + "value": null, + "timestamp": "2021-01-26T20:55:28.411Z" + }, + "pi": { + "value": "shp", + "timestamp": "2019-08-10T18:37:20.457Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2019-08-10T18:37:20.534Z" + } + }, + "remoteControlStatus": { + "remoteControlEnabled": { + "value": "false", + "timestamp": "2025-04-06T17:30:05.372Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 22100103, + "timestamp": "2022-11-01T11:53:01.255Z" + } + }, + "refresh": {}, + "samsungce.washerOperatingState": { + "washerJobState": { + "value": "none", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "operatingState": { + "value": "ready", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "supportedOperatingStates": { + "value": ["ready", "running", "paused"], + "timestamp": "2022-11-01T11:53:01.255Z" + }, + "scheduledJobs": { + "value": null + }, + "scheduledPhases": { + "value": null + }, + "progress": { + "value": null + }, + "remainingTimeStr": { + "value": "00:57", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "washerJobPhase": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": 57, + "unit": "min", + "timestamp": "2025-04-18T13:17:00.432Z" + } + }, + "execute": { + "data": { + "value": null, + "data": {}, + "timestamp": "2020-10-05T02:10:50.602Z" + } + }, + "washerOperatingState": { + "completionTime": { + "value": "2025-04-18T14:14:00Z", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "machineState": { + "value": "stop", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "washerJobState": { + "value": "none", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "supportedMachineStates": { + "value": null, + "timestamp": "2020-08-14T14:25:00.803Z" + } + }, + "switch": { + "switch": { + "value": null, + "timestamp": "2020-09-13T18:32:28.637Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json b/tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json new file mode 100644 index 00000000000..c1a4cd12578 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json @@ -0,0 +1,84 @@ +{ + "items": [ + { + "deviceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "name": "Washer", + "label": "Washer", + "manufacturerName": "Samsung Electronics", + "presentationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "ownerId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "deviceTypeName": "Samsung OCF Washer", + "components": [ + { + "id": "main", + "label": "Washer", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "remoteControlStatus", + "version": 1 + }, + { + "id": "washerOperatingState", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.washerOperatingState", + "version": 1 + } + ], + "categories": [ + { + "name": "Washer", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2019-08-10T18:37:20Z", + "profile": { + "id": "REDACTED" + }, + "ocf": { + "ocfDeviceType": "oic.d.washer", + "name": "Washer", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0", + "manufacturerName": "Samsung Electronics", + "modelNumber": "TP6X_WA54M8750AV|20183944|20000101001111000100000000000000", + "vendorId": "DA-WM-WM-100001", + "lastSignupTime": "2021-01-16T06:29:39.379382Z", + "transferCandidate": false, + "additionalAuthCodeRequired": false + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": null, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 14cdd1548fc..61cecdbd364 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -2089,6 +2089,101 @@ 'state': 'on', }) # --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washer_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_switch_switch_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Washer Power', + }), + 'context': , + 'entity_id': 'binary_sensor.washer_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washer_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remote control', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remote_control', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer Remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.washer_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[ecobee_sensor][binary_sensor.child_bedroom_motion-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index c10f47289a9..d70d9a1dcfc 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -992,6 +992,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_wm_wm_100001] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'TP6X_WA54M8750AV', + 'model_id': None, + 'name': 'Washer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[ecobee_sensor] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index b6528edfebe..17d8e10d230 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -525,3 +525,61 @@ 'state': 'standard', }) # --- +# name: test_all_entities[da_wm_wm_100001][select.washer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'run', + 'pause', + 'stop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.washer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'operating_state', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][select.washer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer', + 'options': list([ + 'run', + 'pause', + 'stop', + ]), + }), + 'context': , + 'entity_id': 'select.washer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stop', + }) +# --- diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 0e9ddf2ea09..a8d4da9123c 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -8546,6 +8546,198 @@ 'state': '1642.2', }) # --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_completion_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_completion_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Completion time', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'completion_time', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_completionTime_completionTime', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_completion_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Washer Completion time', + }), + 'context': , + 'entity_id': 'sensor.washer_completion_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-18T14:14:00+00:00', + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_job_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'air_wash', + 'ai_rinse', + 'ai_spin', + 'ai_wash', + 'cooling', + 'delay_wash', + 'drying', + 'finish', + 'none', + 'pre_wash', + 'rinse', + 'spin', + 'wash', + 'weight_sensing', + 'wrinkle_prevent', + 'freeze_protection', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_job_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Job state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'washer_job_state', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_washerJobState_washerJobState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_job_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washer Job state', + 'options': list([ + 'air_wash', + 'ai_rinse', + 'ai_spin', + 'ai_wash', + 'cooling', + 'delay_wash', + 'drying', + 'finish', + 'none', + 'pre_wash', + 'rinse', + 'spin', + 'wash', + 'weight_sensing', + 'wrinkle_prevent', + 'freeze_protection', + ]), + }), + 'context': , + 'entity_id': 'sensor.washer_job_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_machine_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pause', + 'run', + 'stop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_machine_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Machine state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'washer_machine_state', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_machine_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washer Machine state', + 'options': list([ + 'pause', + 'run', + 'stop', + ]), + }), + 'context': , + 'entity_id': 'sensor.washer_machine_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stop', + }) +# --- # name: test_all_entities[ecobee_sensor][sensor.child_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From e7c310ca58d1d945a155727309c7c1e140cc7252 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 7 May 2025 13:55:43 +0300 Subject: [PATCH 097/110] Add missing device_class translations for template helper (#144392) --- homeassistant/components/template/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 66864a027ba..c27acc37ed9 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -292,6 +292,7 @@ "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", + "blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", @@ -302,6 +303,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", From b4ab9177b8118e4c6304e15eb84008f6f127180d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 13:00:19 +0200 Subject: [PATCH 098/110] Bump pySmartThings to 3.2.1 (#144393) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 0f43c2f9790..043bdea71e2 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.0"] + "requirements": ["pysmartthings==3.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0d0380dd1eb..6a170544b40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2326,7 +2326,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.0 +pysmartthings==3.2.1 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ece75eec2ed..fc65b434bba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1899,7 +1899,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.0 +pysmartthings==3.2.1 # homeassistant.components.smarty pysmarty2==0.10.2 From f85d4afe45986a0411012b22460f8e019a3f5d77 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 13:06:53 +0200 Subject: [PATCH 099/110] Set SmartThings power energy state class to Total (#144395) --- .../components/smartthings/sensor.py | 2 +- .../smartthings/snapshots/test_sensor.ambr | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 09287448fe5..2d6451fa279 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -631,7 +631,7 @@ CAPABILITY_TO_SENSORS: dict[ SmartThingsSensorEntityDescription( key="powerEnergy_meter", translation_key="power_energy", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value_fn=lambda value: value["powerEnergy"] / 1000, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index a8d4da9123c..ad073a1d670 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -1364,7 +1364,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1402,7 +1402,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'AC Office Granit Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1793,7 +1793,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1831,7 +1831,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Office AirFree Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -2222,7 +2222,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2260,7 +2260,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Aire Dormitorio Principal Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4000,7 +4000,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4038,7 +4038,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Refrigerator Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4277,7 +4277,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4315,7 +4315,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Refrigerator Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4554,7 +4554,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4592,7 +4592,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Frigo Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5128,7 +5128,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -5166,7 +5166,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Eco Heating System Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5637,7 +5637,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -5675,7 +5675,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Dishwasher Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -6104,7 +6104,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -6142,7 +6142,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'AirDresser Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -6571,7 +6571,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -6609,7 +6609,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Dryer Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7038,7 +7038,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -7076,7 +7076,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Seca-Roupa Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7507,7 +7507,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -7545,7 +7545,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Washer Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7976,7 +7976,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -8014,7 +8014,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Washing Machine Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -8445,7 +8445,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -8483,7 +8483,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Machine à Laver Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , From aa2b61f13344fded3706b3e0cbf64b16ca18a6d6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 12:55:28 +0200 Subject: [PATCH 100/110] Fix variables in MELCloud (#144396) --- homeassistant/components/melcloud/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 682a28ea080..19c333e5825 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -57,8 +57,8 @@ ATA_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATA_HVAC_MODE_LOOKUP.items()} ATW_ZONE_HVAC_MODE_LOOKUP = { - atw.ZONE_OPERATION_MODE_HEAT: HVACMode.HEAT, - atw.ZONE_OPERATION_MODE_COOL: HVACMode.COOL, + atw.ZONE_STATUS_HEAT: HVACMode.HEAT, + atw.ZONE_STATUS_COOL: HVACMode.COOL, } ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATW_ZONE_HVAC_MODE_LOOKUP.items()} From c98ba7f6ba7ed75da2eb0bb34d8e1252a13607be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 May 2025 11:09:32 +0000 Subject: [PATCH 101/110] Bump version to 2025.5.0b9 --- 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 224a3eaee21..94d5d1062af 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index c81b7c1616f..2e09de0c1a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b8" +version = "2025.5.0b9" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From a23644debcadecc7094fb10a47f44bdcd5852ff7 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 1 May 2025 16:36:05 +0200 Subject: [PATCH 102/110] Fix test in Husqvarna Automower (#144055) --- .../husqvarna_automower/test_lawn_mower.py | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 044989e5cf0..a8c34a3fc79 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -21,37 +21,42 @@ from .const import TEST_MOWER_ID from tests.common import MockConfigEntry, async_fire_time_changed -async def test_lawn_mower_states( - hass: HomeAssistant, - mock_automower_client: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - values: dict[str, MowerAttributes], -) -> None: - """Test lawn_mower state.""" - await setup_integration(hass, mock_config_entry) - state = hass.states.get("lawn_mower.test_mower_1") - assert state is not None - assert state.state == LawnMowerActivity.DOCKED - - for activity, state, expected_state in ( +@pytest.mark.parametrize( + ("activity", "mower_state", "expected_state"), + [ (MowerActivities.UNKNOWN, MowerStates.PAUSED, LawnMowerActivity.PAUSED), - (MowerActivities.MOWING, MowerStates.NOT_APPLICABLE, LawnMowerActivity.MOWING), + (MowerActivities.MOWING, MowerStates.IN_OPERATION, LawnMowerActivity.MOWING), (MowerActivities.NOT_APPLICABLE, MowerStates.ERROR, LawnMowerActivity.ERROR), ( MowerActivities.GOING_HOME, MowerStates.IN_OPERATION, LawnMowerActivity.RETURNING, ), - ): - values[TEST_MOWER_ID].mower.activity = activity - values[TEST_MOWER_ID].mower.state = state - mock_automower_client.get_status.return_value = values - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - state = hass.states.get("lawn_mower.test_mower_1") - assert state.state == expected_state + ], +) +async def test_lawn_mower_states( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + values: dict[str, MowerAttributes], + activity: MowerActivities, + mower_state: MowerStates, + expected_state: LawnMowerActivity, +) -> None: + """Test lawn_mower state.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("lawn_mower.test_mower_1") + assert state is not None + assert state.state == LawnMowerActivity.DOCKED + values[TEST_MOWER_ID].mower.activity = activity + values[TEST_MOWER_ID].mower.state = mower_state + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("lawn_mower.test_mower_1") + assert state.state == expected_state @pytest.mark.parametrize( From 7eb690b125a9d86eaf674e94e0f8b98fa022146f Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 7 May 2025 13:12:10 +0200 Subject: [PATCH 103/110] Improve activity logic in Husqvarna Automower (#144057) * Improve activity logic in Husqvarna Automower * add test --- homeassistant/components/husqvarna_automower/lawn_mower.py | 6 +++--- tests/components/husqvarna_automower/test_lawn_mower.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index ee6007f089b..9ae214524a7 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -110,10 +110,10 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): mower_attributes = self.mower_attributes if mower_attributes.mower.state in PAUSED_STATES: return LawnMowerActivity.PAUSED - if mower_attributes.mower.activity in MOWING_ACTIVITIES: + if mower_attributes.mower.state in MowerStates.IN_OPERATION: + if mower_attributes.mower.activity == MowerActivities.GOING_HOME: + return LawnMowerActivity.RETURNING return LawnMowerActivity.MOWING - if mower_attributes.mower.activity == MowerActivities.GOING_HOME: - return LawnMowerActivity.RETURNING if (mower_attributes.mower.state == "RESTRICTED") or ( mower_attributes.mower.activity in DOCKED_ACTIVITIES ): diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index a8c34a3fc79..12c53d709ca 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -32,6 +32,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed MowerStates.IN_OPERATION, LawnMowerActivity.RETURNING, ), + ( + MowerActivities.NOT_APPLICABLE, + MowerStates.IN_OPERATION, + LawnMowerActivity.MOWING, + ), ], ) async def test_lawn_mower_states( From 2d40b1ec75677b9336ae8e1973f131f37d8a21ac Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 7 May 2025 13:09:43 +0200 Subject: [PATCH 104/110] Bump devolo_home_control_api to 0.19.0 (#144374) --- .../components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/devolo_home_control/mocks.py | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index a9715fffa84..983b2a33452 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -8,6 +8,6 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["devolo_home_control_api"], - "requirements": ["devolo-home-control-api==0.18.3"], + "requirements": ["devolo-home-control-api==0.19.0"], "zeroconf": ["_dvl-deviceapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6a170544b40..5dd3bc69152 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ denonavr==1.0.1 devialet==1.5.7 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.18.3 +devolo-home-control-api==0.19.0 # homeassistant.components.devolo_home_network devolo-plc-api==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc65b434bba..c2461b19fe6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ denonavr==1.0.1 devialet==1.5.7 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.18.3 +devolo-home-control-api==0.19.0 # homeassistant.components.devolo_home_network devolo-plc-api==1.5.1 diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index d611c73cf2c..24f4e64ffe6 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -1,5 +1,6 @@ """Mocks for tests.""" +from datetime import UTC from typing import Any from unittest.mock import MagicMock @@ -28,6 +29,7 @@ class BinarySensorPropertyMock(BinarySensorProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.element_uid = "Test" self.key_count = 1 self.sensor_type = "door" @@ -41,6 +43,7 @@ class BinarySwitchPropertyMock(BinarySwitchProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.element_uid = "Test" self.state = False @@ -51,6 +54,7 @@ class ConsumptionPropertyMock(ConsumptionProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.element_uid = "devolo.Meter:Test" self.current_unit = "W" self.total_unit = "kWh" @@ -68,6 +72,7 @@ class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): self._unit = "°C" self._value = 20 self._logger = MagicMock() + self._timezone = UTC class BrightnessSensorPropertyMock(MultiLevelSensorProperty): @@ -80,6 +85,7 @@ class BrightnessSensorPropertyMock(MultiLevelSensorProperty): self._unit = "%" self._value = 20 self._logger = MagicMock() + self._timezone = UTC class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): @@ -92,6 +98,7 @@ class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): self.max = 24 self._value = 20 self._logger = MagicMock() + self._timezone = UTC class SirenPropertyMock(MultiLevelSwitchProperty): @@ -105,6 +112,7 @@ class SirenPropertyMock(MultiLevelSwitchProperty): self.switch_type = "tone" self._value = 0 self._logger = MagicMock() + self._timezone = UTC class SettingsMock(SettingsProperty): @@ -113,6 +121,7 @@ class SettingsMock(SettingsProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.name = "Test" self.zone = "Test" self.tone = 1 From 9556285c5902a8dab425c0962e2faf76fe0c5e70 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 7 May 2025 14:36:28 +0200 Subject: [PATCH 105/110] Bump deebot-client to 13.1.0 (#144397) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 2a332e498c7..e670a36cf72 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==13.0.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5dd3bc69152..b3fa849d2d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ debugpy==1.8.13 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.0.1 +deebot-client==13.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2461b19fe6..b3487703208 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -653,7 +653,7 @@ dbus-fast==2.43.0 debugpy==1.8.13 # homeassistant.components.ecovacs -deebot-client==13.0.1 +deebot-client==13.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From fb01a0a9f13704e3dc5cd0adbef8aa9c53d25f83 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 May 2025 14:14:39 +0200 Subject: [PATCH 106/110] Update frontend to 20250507.0 (#144398) --- 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 4abf9aa7814..84062384bf5 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==20250506.0"] + "requirements": ["home-assistant-frontend==20250507.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1838e552800..9599a4fbfb4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250506.0 +home-assistant-frontend==20250507.0 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b3fa849d2d1..a5388e487c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250506.0 +home-assistant-frontend==20250507.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3487703208..f4058800bd7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250506.0 +home-assistant-frontend==20250507.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From d4e99efc464c25973a1109a63ae3c2a219740f76 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 7 May 2025 15:08:17 +0300 Subject: [PATCH 107/110] Add more missing device_class translations for template helper (#144399) --- homeassistant/components/template/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index c27acc37ed9..0b431d661cd 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -290,6 +290,7 @@ "options": { "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "area": "[%key:component::sensor::entity_component::area::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", "blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]", @@ -340,6 +341,7 @@ "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, From 999e930fc8af068ea1517b9f6d784311bbe4c92e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 May 2025 13:04:15 +0000 Subject: [PATCH 108/110] Bump version to 2025.5.0b10 --- 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 94d5d1062af..94368e9cef0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 2e09de0c1a7..2db79a8364f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b9" +version = "2025.5.0b10" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 43d8345821cb1eb3be5a11bd94217734f17a8470 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 May 2025 09:05:08 -0500 Subject: [PATCH 109/110] Bump intents to 2025.5.7 (#144404) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 3cf4d826a9d..2955bb96833 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==2.2.3", "home-assistant-intents==2025.4.30"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9599a4fbfb4..48b21942f4d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250507.0 -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index 2db79a8364f..b2cdca02ee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ # onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its # dependencies to stage 0. - "home-assistant-intents==2025.4.30", + "home-assistant-intents==2025.5.7", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index e8b9e12bfe0..26ff191025f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.96.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index a5388e487c6..da5bdddd5c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ holidays==0.70 home-assistant-frontend==20250507.0 # homeassistant.components.conversation -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4058800bd7..c7f6f484a70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ holidays==0.70 home-assistant-frontend==20250507.0 # homeassistant.components.conversation -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 9248fd73cb3..306b5901370 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,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.25.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.4.30 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 18f2b120ef0496b148c9c1ef9aaaeeee9717311a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 May 2025 14:31:26 +0000 Subject: [PATCH 110/110] Bump version to 2025.5.0 --- 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 94368e9cef0..11abbd33b41 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index b2cdca02ee2..50cc169cf10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0b10" +version = "2025.5.0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."