From c33372686754467093b4686a851a40d3f1a739ee Mon Sep 17 00:00:00 2001 From: wuede Date: Sun, 18 May 2025 23:00:36 +0200 Subject: [PATCH 01/38] Netatmo: do not fail on schedule updates (#142933) * do not fail on schedule updates * add test to check that the store data remains unchanged --- homeassistant/components/netatmo/climate.py | 29 ++++++++++++--------- tests/components/netatmo/test_climate.py | 28 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 2e3d8c6bcb8..f8f89ffd06b 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -248,19 +248,22 @@ class NetatmoThermostat(NetatmoRoomEntity, ClimateEntity): if self.home.entity_id != data["home_id"]: return - if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: - self._selected_schedule = getattr( - self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( - data["schedule_id"] - ), - "name", - None, - ) - self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( - self._selected_schedule - ) - self.async_write_ha_state() - self.data_handler.async_force_update(self._signal_name) + if data["event_type"] == EVENT_TYPE_SCHEDULE: + # handle schedule change + if "schedule_id" in data: + self._selected_schedule = getattr( + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( + data["schedule_id"] + ), + "name", + None, + ) + self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( + self._selected_schedule + ) + self.async_write_ha_state() + self.data_handler.async_force_update(self._signal_name) + # ignore other schedule events return home = data["home"] diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 18c811fd76b..45216e415a5 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -66,6 +66,34 @@ async def test_entity( ) +async def test_schedule_update_webhook_event( + hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock +) -> None: + """Test schedule update webhook event without schedule_id.""" + + with selected_platforms([Platform.CLIMATE]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + climate_entity_livingroom = "climate.livingroom" + + # Save initial state + initial_state = hass.states.get(climate_entity_livingroom) + + # Create a schedule update event without a schedule_id (the event is sent when temperature sets of a schedule are changed) + response = { + "home_id": "91763b24c43d3e344f424e8b", + "event_type": "schedule", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + # State should be unchanged + assert hass.states.get(climate_entity_livingroom) == initial_state + + async def test_webhook_event_handling_thermostats( hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock ) -> None: From 78351ff7a7e37fb3754efc756013df77ed7f54f5 Mon Sep 17 00:00:00 2001 From: disforw Date: Mon, 19 May 2025 15:47:01 -0400 Subject: [PATCH 02/38] Fix QNAP fail to load (#144675) * Update coordinator.py * Update coordinator.py @peternash * Update coordinator.py * Update coordinator.py * Update coordinator.py * Update coordinator.py --- homeassistant/components/qnap/coordinator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/qnap/coordinator.py b/homeassistant/components/qnap/coordinator.py index a6d654ddbbd..8b6cb930b4f 100644 --- a/homeassistant/components/qnap/coordinator.py +++ b/homeassistant/components/qnap/coordinator.py @@ -6,6 +6,7 @@ from contextlib import contextmanager, nullcontext from datetime import timedelta import logging from typing import Any +import warnings from qnapstats import QNAPStats import urllib3 @@ -37,7 +38,8 @@ def suppress_insecure_request_warning(): Was added in here to solve the following issue, not being solved upstream. https://github.com/colinodell/python-qnapstats/issues/96 """ - with urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", urllib3.exceptions.InsecureRequestWarning) yield From 926502b0f1c01fdb99ba3cd5ea12e0e834f1df3f Mon Sep 17 00:00:00 2001 From: TheOneValen <4579392+TheOneValen@users.noreply.github.com> Date: Wed, 21 May 2025 17:18:34 +0200 Subject: [PATCH 03/38] Allow image send with read-only access (matrix notify) (#144819) --- homeassistant/components/matrix/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 8640aa4d074..5123436a397 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -475,7 +475,7 @@ class MatrixBot: file_stat = await aiofiles.os.stat(image_path) _LOGGER.debug("Uploading file from path, %s", image_path) - async with aiofiles.open(image_path, "r+b") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: response, _ = await self._client.upload( image_file, content_type=mime_type, From 47140e14d9e1f53e8df14d42bf9807c1aa7eb511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Sun, 18 May 2025 17:23:21 +0200 Subject: [PATCH 04/38] Postpone update in WMSPro after service call (#144836) * Reduce stress on WMS WebControl pro with higher scan interval Avoid delays and connection issues due to overloaded hub. Fixes #133832 and #134413 * Schedule an entity state update after performing an action Avoid delaying immediate status updates, e.g. on/off changes. * Replace scheduled state updates with delayed action completion Suggested-by: joostlek --- homeassistant/components/wmspro/cover.py | 8 +++++++- homeassistant/components/wmspro/light.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wmspro/cover.py b/homeassistant/components/wmspro/cover.py index d46ffa6dab6..0c25c1b277f 100644 --- a/homeassistant/components/wmspro/cover.py +++ b/homeassistant/components/wmspro/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta from typing import Any @@ -17,7 +18,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import WebControlProConfigEntry from .entity import WebControlProGenericEntity -SCAN_INTERVAL = timedelta(seconds=5) +ACTION_DELAY = 0.5 +SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 @@ -56,6 +58,7 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): """Move the cover to a specific position.""" action = self._dest.action(self._drive_action_desc) await action(percentage=100 - kwargs[ATTR_POSITION]) + await asyncio.sleep(ACTION_DELAY) @property def is_closed(self) -> bool | None: @@ -66,11 +69,13 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): """Open the cover.""" action = self._dest.action(self._drive_action_desc) await action(percentage=0) + await asyncio.sleep(ACTION_DELAY) async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" action = self._dest.action(self._drive_action_desc) await action(percentage=100) + await asyncio.sleep(ACTION_DELAY) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the device if in motion.""" @@ -79,6 +84,7 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): WMS_WebControl_pro_API_actionType.Stop, ) await action() + await asyncio.sleep(ACTION_DELAY) class WebControlProAwning(WebControlProCover): diff --git a/homeassistant/components/wmspro/light.py b/homeassistant/components/wmspro/light.py index d181beb1eaa..c1aeb230cab 100644 --- a/homeassistant/components/wmspro/light.py +++ b/homeassistant/components/wmspro/light.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta from typing import Any @@ -16,7 +17,8 @@ from . import WebControlProConfigEntry from .const import BRIGHTNESS_SCALE from .entity import WebControlProGenericEntity -SCAN_INTERVAL = timedelta(seconds=5) +ACTION_DELAY = 0.5 +SCAN_INTERVAL = timedelta(seconds=15) PARALLEL_UPDATES = 1 @@ -54,11 +56,13 @@ class WebControlProLight(WebControlProGenericEntity, LightEntity): """Turn the light on.""" action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch) await action(onOffState=True) + await asyncio.sleep(ACTION_DELAY) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch) await action(onOffState=False) + await asyncio.sleep(ACTION_DELAY) class WebControlProDimmer(WebControlProLight): @@ -87,3 +91,4 @@ class WebControlProDimmer(WebControlProLight): await action( percentage=brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS]) ) + await asyncio.sleep(ACTION_DELAY) From 41be82f167a1200a28e91034a7068f97b5f562d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 May 2025 16:23:04 -0400 Subject: [PATCH 05/38] Bump ESPHome stable BLE version to 2025.5.0 (#144857) --- homeassistant/components/esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index f793fd16bfe..2c9bee32734 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False DEFAULT_PORT: Final = 6053 -STABLE_BLE_VERSION_STR = "2025.2.2" +STABLE_BLE_VERSION_STR = "2025.5.0" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", From 9bb9132e7b3356cc3bfcc72bcfb755e803921059 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sun, 18 May 2025 22:37:06 +0100 Subject: [PATCH 06/38] Fix album and artist returning "None" rather than None for Squeezebox media player. (#144971) * fix * snapshot update * cast type --- homeassistant/components/squeezebox/media_player.py | 10 +++++----- .../squeezebox/snapshots/test_media_player.ambr | 4 ---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6e99099ccb1..d90e24affbb 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -6,7 +6,7 @@ from collections.abc import Callable from datetime import datetime import json import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from pysqueezebox import Server, async_discover import voluptuous as vol @@ -329,22 +329,22 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): @property def media_title(self) -> str | None: """Title of current playing media.""" - return str(self._player.title) + return cast(str | None, self._player.title) @property def media_channel(self) -> str | None: """Channel (e.g. webradio name) of current playing media.""" - return str(self._player.remote_title) + return cast(str | None, self._player.remote_title) @property def media_artist(self) -> str | None: """Artist of current playing media.""" - return str(self._player.artist) + return cast(str | None, self._player.artist) @property def media_album_name(self) -> str | None: """Album of current playing media.""" - return str(self._player.album) + return cast(str | None, self._player.album) @property def repeat(self) -> RepeatMode: diff --git a/tests/components/squeezebox/snapshots/test_media_player.ambr b/tests/components/squeezebox/snapshots/test_media_player.ambr index c0633035a84..7540a448882 100644 --- a/tests/components/squeezebox/snapshots/test_media_player.ambr +++ b/tests/components/squeezebox/snapshots/test_media_player.ambr @@ -78,12 +78,8 @@ 'group_members': list([ ]), 'is_volume_muted': True, - 'media_album_name': 'None', - 'media_artist': 'None', - 'media_channel': 'None', 'media_duration': 1, 'media_position': 1, - 'media_title': 'None', 'query_result': dict({ }), 'repeat': , From 642e7fd487aec255034e9f715e4d01e6628e2344 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sat, 17 May 2025 13:06:02 +0200 Subject: [PATCH 07/38] Bump aiontfy to 0.5.2 (#145044) --- homeassistant/components/ntfy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ntfy/fixtures/account.json | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ntfy/manifest.json b/homeassistant/components/ntfy/manifest.json index 95204444fbb..fde1569d622 100644 --- a/homeassistant/components/ntfy/manifest.json +++ b/homeassistant/components/ntfy/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["aionfty"], "quality_scale": "bronze", - "requirements": ["aiontfy==0.5.1"] + "requirements": ["aiontfy==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 79ae3501792..3b597a61245 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -319,7 +319,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.1 +aiontfy==0.5.2 # homeassistant.components.nut aionut==4.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3686c8e39b..b865aa200ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -301,7 +301,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.1 +aiontfy==0.5.2 # homeassistant.components.nut aionut==4.3.4 diff --git a/tests/components/ntfy/fixtures/account.json b/tests/components/ntfy/fixtures/account.json index 8b4ee501a4d..29a96beb23b 100644 --- a/tests/components/ntfy/fixtures/account.json +++ b/tests/components/ntfy/fixtures/account.json @@ -55,5 +55,12 @@ "reservations_remaining": 2, "attachment_total_size": 0, "attachment_total_size_remaining": 104857600 + }, + "billing": { + "customer": true, + "subscription": true, + "status": "active", + "interval": "year", + "paid_until": 1754080667 } } From 8e44684a61c63775bacb1bfbde58e1edc84a2bde Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sat, 17 May 2025 16:44:53 +0200 Subject: [PATCH 08/38] Fix proberly Ecovacs mower area sensors (#145078) --- homeassistant/components/ecovacs/sensor.py | 4 ++ .../ecovacs/snapshots/test_sensor.ambr | 48 ++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index a8600d786a8..eab642119e4 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -78,7 +78,9 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.area, translation_key="stats_area", + device_class=SensorDeviceClass.AREA, native_unit_of_measurement_fn=get_area_native_unit_of_measurement, + suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS, ), EcovacsSensorEntityDescription[StatsEvent]( key="stats_time", @@ -95,8 +97,10 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( value_fn=lambda e: e.area, key="total_stats_area", translation_key="total_stats_area", + device_class=SensorDeviceClass.AREA, native_unit_of_measurement_fn=get_area_native_unit_of_measurement, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS, ), EcovacsSensorEntityDescription[TotalStatsEvent]( capability_fn=lambda caps: caps.stats.total, diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 7fa7a41234d..c78df0e189a 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -172,8 +172,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Area cleaned', 'platform': 'ecovacs', @@ -181,21 +184,22 @@ 'supported_features': 0, 'translation_key': 'stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_stats_area', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Goat G1 Area cleaned', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_area_cleaned', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10', + 'state': '0.0010', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_battery:entity-registry] @@ -514,8 +518,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total area cleaned', 'platform': 'ecovacs', @@ -523,22 +530,23 @@ 'supported_features': 0, 'translation_key': 'total_stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Goat G1 Total area cleaned', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_total_area_cleaned', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '60', + 'state': '0.0060', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:entity-registry] @@ -762,8 +770,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Area cleaned', 'platform': 'ecovacs', @@ -777,6 +788,7 @@ # name: test_sensors[qhe2o2][sensor.dusty_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Dusty Area cleaned', 'unit_of_measurement': , }), @@ -1257,8 +1269,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total area cleaned', 'platform': 'ecovacs', @@ -1272,6 +1287,7 @@ # name: test_sensors[qhe2o2][sensor.dusty_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Dusty Total area cleaned', 'state_class': , 'unit_of_measurement': , @@ -1553,8 +1569,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Area cleaned', 'platform': 'ecovacs', @@ -1568,6 +1587,7 @@ # name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Ozmo 950 Area cleaned', 'unit_of_measurement': , }), @@ -1943,8 +1963,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total area cleaned', 'platform': 'ecovacs', @@ -1958,6 +1981,7 @@ # name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Ozmo 950 Total area cleaned', 'state_class': , 'unit_of_measurement': , From 422dbfef88ccfa20d9387858ffea5f772043e32a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 19 May 2025 12:09:27 +0200 Subject: [PATCH 09/38] Map auto to heat_cool for thermostat in SmartThings (#145098) --- homeassistant/components/smartthings/climate.py | 4 ++-- tests/components/smartthings/snapshots/test_climate.ambr | 4 ++-- tests/components/smartthings/test_climate.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 7cb3b0210bb..9de11f4af71 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -31,7 +31,7 @@ from .entity import SmartThingsEntity ATTR_OPERATION_STATE = "operation_state" MODE_TO_STATE = { - "auto": HVACMode.AUTO, + "auto": HVACMode.HEAT_COOL, "cool": HVACMode.COOL, "eco": HVACMode.AUTO, "rush hour": HVACMode.AUTO, @@ -40,7 +40,7 @@ MODE_TO_STATE = { "off": HVACMode.OFF, } STATE_TO_MODE = { - HVACMode.AUTO: "auto", + HVACMode.HEAT_COOL: "auto", HVACMode.COOL: "cool", HVACMode.HEAT: "heat", HVACMode.OFF: "off", diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index b23e7024e05..6f4dd67d7f7 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -541,7 +541,7 @@ 'hvac_modes': list([ , , - , + , ]), 'max_temp': 35.0, 'min_temp': 7.0, @@ -589,7 +589,7 @@ 'hvac_modes': list([ , , - , + , ]), 'max_temp': 35.0, 'min_temp': 7.0, diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 8241e6de3b3..9e3fa22f55d 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -625,7 +625,7 @@ async def test_thermostat_set_hvac_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.AUTO}, + {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, blocking=True, ) devices.execute_device_command.assert_called_once_with( From 9534a919ce189ae034be0a7ad8ad0af7891e6ded Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 17 May 2025 20:18:14 +0200 Subject: [PATCH 10/38] Add missing device condition translations to lock component (#145104) --- homeassistant/components/lock/strings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index fd2854b7932..46788e5a310 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -9,7 +9,11 @@ "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked", - "is_open": "{entity_name} is open" + "is_open": "{entity_name} is open", + "is_jammed": "{entity_name} is jammed", + "is_locking": "{entity_name} is locking", + "is_unlocking": "{entity_name} is unlocking", + "is_opening": "{entity_name} is opening" }, "trigger_type": { "locked": "{entity_name} locked", From a17275b559c4762724bbc029c33be17a6d41f251 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 19 May 2025 11:03:59 -0700 Subject: [PATCH 11/38] Fix history_stats with sliding window that ends before now (#145117) --- .../components/history_stats/data.py | 64 +++++++---- tests/components/history_stats/test_sensor.py | 101 +++++++++++++++++- 2 files changed, 139 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 756a6b3ce9d..fd950dbba23 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -60,6 +60,9 @@ class HistoryStats: self._start = start self._end = end + self._pending_events: list[Event[EventStateChangedData]] = [] + self._query_count = 0 + async def async_update( self, event: Event[EventStateChangedData] | None ) -> HistoryStatsState: @@ -85,6 +88,14 @@ class HistoryStats: utc_now = dt_util.utcnow() now_timestamp = floored_timestamp(utc_now) + # If we end up querying data from the recorder when we get triggered by a new state + # change event, it is possible this function could be reentered a second time before + # the first recorder query returns. In that case a second recorder query will be done + # and we need to hold the new event so that we can append it after the second query. + # Otherwise the event will be dropped. + if event: + self._pending_events.append(event) + if current_period_start_timestamp > now_timestamp: # History cannot tell the future self._history_current_period = [] @@ -113,15 +124,14 @@ class HistoryStats: start_changed = ( current_period_start_timestamp != previous_period_start_timestamp ) + end_changed = current_period_end_timestamp != previous_period_end_timestamp if start_changed: self._prune_history_cache(current_period_start_timestamp) new_data = False if event and (new_state := event.data["new_state"]) is not None: - if ( - current_period_start_timestamp - <= floored_timestamp(new_state.last_changed) - <= current_period_end_timestamp + if current_period_start_timestamp <= floored_timestamp( + new_state.last_changed ): self._history_current_period.append( HistoryState(new_state.state, new_state.last_changed_timestamp) @@ -131,26 +141,31 @@ class HistoryStats: not new_data and current_period_end_timestamp < now_timestamp and not start_changed + and not end_changed ): # If period has not changed and current time after the period end... # Don't compute anything as the value cannot have changed return self._state else: await self._async_history_from_db( - current_period_start_timestamp, current_period_end_timestamp + current_period_start_timestamp, now_timestamp ) - if event and (new_state := event.data["new_state"]) is not None: - if ( - current_period_start_timestamp - <= floored_timestamp(new_state.last_changed) - <= current_period_end_timestamp - ): - self._history_current_period.append( - HistoryState(new_state.state, new_state.last_changed_timestamp) - ) + for pending_event in self._pending_events: + if (new_state := pending_event.data["new_state"]) is not None: + if current_period_start_timestamp <= floored_timestamp( + new_state.last_changed + ): + self._history_current_period.append( + HistoryState( + new_state.state, new_state.last_changed_timestamp + ) + ) self._has_recorder_data = True + if self._query_count == 0: + self._pending_events.clear() + seconds_matched, match_count = self._async_compute_seconds_and_changes( now_timestamp, current_period_start_timestamp, @@ -165,12 +180,16 @@ class HistoryStats: current_period_end_timestamp: float, ) -> None: """Update history data for the current period from the database.""" - instance = get_instance(self.hass) - states = await instance.async_add_executor_job( - self._state_changes_during_period, - current_period_start_timestamp, - current_period_end_timestamp, - ) + self._query_count += 1 + try: + instance = get_instance(self.hass) + states = await instance.async_add_executor_job( + self._state_changes_during_period, + current_period_start_timestamp, + current_period_end_timestamp, + ) + finally: + self._query_count -= 1 self._history_current_period = [ HistoryState(state.state, state.last_changed.timestamp()) for state in states @@ -208,6 +227,9 @@ class HistoryStats: current_state_matches = history_state.state in self._entity_states state_change_timestamp = history_state.last_changed + if math.floor(state_change_timestamp) > end_timestamp: + break + if math.floor(state_change_timestamp) > now_timestamp: # Shouldn't count states that are in the future _LOGGER.debug( @@ -215,7 +237,7 @@ class HistoryStats: state_change_timestamp, now_timestamp, ) - continue + break if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index ee426cf3048..5b98000997e 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1017,6 +1017,18 @@ async def test_start_from_history_then_watch_state_changes_sliding( } for i, sensor_type in enumerate(["time", "ratio", "count"]) ] + + [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": f"sensor_delayed{i}", + "state": "on", + "end": "{{ utcnow()-timedelta(minutes=5) }}", + "duration": {"minutes": 55}, + "type": sensor_type, + } + for i, sensor_type in enumerate(["time", "ratio", "count"]) + ] }, ) await hass.async_block_till_done() @@ -1028,6 +1040,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.0" assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0" + assert hass.states.get("sensor.sensor_delayed0").state == "0.0" + assert hass.states.get("sensor.sensor_delayed1").state == "0.0" + assert hass.states.get("sensor.sensor_delayed2").state == "0" with freeze_time(time): hass.states.async_set("binary_sensor.state", "on") @@ -1038,6 +1053,10 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.0" assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "1" + # Delayed sensor will not have registered the turn on yet + assert hass.states.get("sensor.sensor_delayed0").state == "0.0" + assert hass.states.get("sensor.sensor_delayed1").state == "0.0" + assert hass.states.get("sensor.sensor_delayed2").state == "0" # After sensor has been on for 15 minutes, check state time += timedelta(minutes=15) # 00:15 @@ -1048,6 +1067,10 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.25" assert hass.states.get("sensor.sensor1").state == "25.0" assert hass.states.get("sensor.sensor2").state == "1" + # Delayed sensor will only have data from 00:00 - 00:10 + assert hass.states.get("sensor.sensor_delayed0").state == "0.17" + assert hass.states.get("sensor.sensor_delayed1").state == "18.2" # 10 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" with freeze_time(time): hass.states.async_set("binary_sensor.state", "off") @@ -1064,6 +1087,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.25" assert hass.states.get("sensor.sensor1").state == "25.0" assert hass.states.get("sensor.sensor2").state == "1" + assert hass.states.get("sensor.sensor_delayed0").state == "0.25" + assert hass.states.get("sensor.sensor_delayed1").state == "27.3" # 15 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" time += timedelta(minutes=20) # 01:05 @@ -1075,6 +1101,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.17" assert hass.states.get("sensor.sensor1").state == "16.7" assert hass.states.get("sensor.sensor2").state == "1" + assert hass.states.get("sensor.sensor_delayed0").state == "0.17" + assert hass.states.get("sensor.sensor_delayed1").state == "18.2" # 10 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" time += timedelta(minutes=5) # 01:10 @@ -1086,6 +1115,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.08" assert hass.states.get("sensor.sensor1").state == "8.3" assert hass.states.get("sensor.sensor2").state == "1" + assert hass.states.get("sensor.sensor_delayed0").state == "0.08" + assert hass.states.get("sensor.sensor_delayed1").state == "9.1" # 5 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" time += timedelta(minutes=10) # 01:20 @@ -1096,6 +1128,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.0" assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0" + assert hass.states.get("sensor.sensor_delayed0").state == "0.0" + assert hass.states.get("sensor.sensor_delayed1").state == "0.0" + assert hass.states.get("sensor.sensor_delayed2").state == "0" async def test_does_not_work_into_the_future( @@ -1629,7 +1664,7 @@ async def test_state_change_during_window_rollover( "entity_id": "binary_sensor.state", "name": "sensor1", "state": "on", - "start": "{{ today_at() }}", + "start": "{{ today_at('12:00') if now().hour == 1 else today_at() }}", "end": "{{ now() }}", "type": "time", } @@ -1644,7 +1679,7 @@ async def test_state_change_during_window_rollover( assert hass.states.get("sensor.sensor1").state == "11.0" # Advance 59 minutes, to record the last minute update just before midnight, just like a real system would do. - t2 = start_time + timedelta(minutes=59, microseconds=300) + t2 = start_time + timedelta(minutes=59, microseconds=300) # 23:59 with freeze_time(t2): async_fire_time_changed(hass, t2) await hass.async_block_till_done() @@ -1653,7 +1688,7 @@ async def test_state_change_during_window_rollover( # One minute has passed and the time has now rolled over into a new day, resetting the recorder window. # The sensor will be ON since midnight. - t3 = t2 + timedelta(minutes=1) + t3 = t2 + timedelta(minutes=1) # 00:01 with freeze_time(t3): # The sensor turns off around this time, before the sensor does its normal polled update. hass.states.async_set("binary_sensor.state", "off") @@ -1662,13 +1697,69 @@ async def test_state_change_during_window_rollover( assert hass.states.get("sensor.sensor1").state == "0.0" # More time passes, and the history stats does a polled update again. It should be 0 since the sensor has been off since midnight. - t4 = t3 + timedelta(minutes=10) + # Turn the sensor back on. + t4 = t3 + timedelta(minutes=10) # 00:10 with freeze_time(t4): async_fire_time_changed(hass, t4) await hass.async_block_till_done() + hass.states.async_set("binary_sensor.state", "on") + await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.0" + # Due to time change, start time has now moved into the future. Turn off the sensor. + t5 = t4 + timedelta(hours=1) # 01:10 + with freeze_time(t5): + hass.states.async_set("binary_sensor.state", "off") + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + + # Start time has moved back to start of today. Turn the sensor on at the same time it is recomputed + # Should query the recorder this time due to start time moving backwards in time. + t6 = t5 + timedelta(hours=1) # 02:10 + + def _fake_states_t6(*args, **kwargs): + return { + "binary_sensor.state": [ + ha.State( + "binary_sensor.state", + "off", + last_changed=t6.replace(hour=0, minute=0, second=0, microsecond=0), + ), + ha.State( + "binary_sensor.state", + "on", + last_changed=t6.replace(hour=0, minute=10, second=0, microsecond=0), + ), + ha.State( + "binary_sensor.state", + "off", + last_changed=t6.replace(hour=1, minute=10, second=0, microsecond=0), + ), + ] + } + + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states_t6, + ), + freeze_time(t6), + ): + hass.states.async_set("binary_sensor.state", "on") + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get("sensor.sensor1").state == "1.0" + + # Another hour passes since the re-query. Total 'On' time should be 2 hours (00:10-1:10, 2:10-now (3:10)) + t7 = t6 + timedelta(hours=1) # 03:10 + with freeze_time(t7): + async_fire_time_changed(hass, t7) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sensor1").state == "2.0" + @pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"]) async def test_end_time_with_microseconds_zeroed( @@ -1934,7 +2025,7 @@ async def test_history_stats_handles_floored_timestamps( await async_update_entity(hass, "sensor.sensor1") await hass.async_block_till_done() - assert last_times == (start_time, start_time + timedelta(hours=2)) + assert last_times == (start_time, start_time) async def test_unique_id( From 3e00366a611551b8b807033c8d66249899edafb0 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sun, 18 May 2025 15:45:11 -0400 Subject: [PATCH 12/38] Bump sense-energy to 0.13.8 (#145156) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index fc54fb50064..3e9d6c81881 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.13.7"] + "requirements": ["sense-energy==0.13.8"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 0a21dbf4cc3..33106f0fd1b 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.13.7"] + "requirements": ["sense-energy==0.13.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b597a61245..da4c4e05caa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2713,7 +2713,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.7 +sense-energy==0.13.8 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b865aa200ce..5c43805aaeb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2196,7 +2196,7 @@ securetar==2025.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.7 +sense-energy==0.13.8 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 1b7dd205c701ebe238691067b4995fcd5014ed93 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 14 May 2025 13:08:26 +0200 Subject: [PATCH 13/38] Improve Z-Wave config flow tests (#144871) * Improve Z-Wave config flow tests * Fix test * Use identify check for result type --- tests/components/zwave_js/test_config_flow.py | 204 +++++++++++------- 1 file changed, 125 insertions(+), 79 deletions(-) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index ac420564f3f..f14c94047b9 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -918,15 +918,15 @@ async def test_usb_discovery_migration( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 @@ -936,13 +936,13 @@ async def test_usb_discovery_migration( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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["type"] is 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}) @@ -954,7 +954,7 @@ async def test_usb_discovery_migration( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -1052,15 +1052,15 @@ async def test_usb_discovery_migration_driver_ready_timeout( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 @@ -1070,13 +1070,13 @@ async def test_usb_discovery_migration_driver_ready_timeout( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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 result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is 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}) @@ -1088,7 +1088,7 @@ async def test_usb_discovery_migration_driver_ready_timeout( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -3712,22 +3712,22 @@ async def test_reconfigure_migrate_with_addon( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 @@ -3737,13 +3737,13 @@ async def test_reconfigure_migrate_with_addon( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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 result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" assert result["data_schema"].schema[CONF_USB_PATH] @@ -3754,7 +3754,7 @@ async def test_reconfigure_migrate_with_addon( }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( "core_zwave_js", AddonsOptions(config={"device": "/test"}) @@ -3766,7 +3766,7 @@ async def test_reconfigure_migrate_with_addon( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -3854,22 +3854,22 @@ async def test_reconfigure_migrate_driver_ready_timeout( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 @@ -3879,13 +3879,13 @@ async def test_reconfigure_migrate_driver_ready_timeout( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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 result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" assert result["data_schema"].schema[CONF_USB_PATH] @@ -3896,7 +3896,7 @@ async def test_reconfigure_migrate_driver_ready_timeout( }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( "core_zwave_js", AddonsOptions(config={"device": "/test"}) @@ -3908,7 +3908,7 @@ async def test_reconfigure_migrate_driver_ready_timeout( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -3944,19 +3944,19 @@ async def test_reconfigure_migrate_backup_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "backup_failed" @@ -3979,30 +3979,28 @@ async def test_reconfigure_migrate_backup_file_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch( - "pathlib.Path.write_bytes", MagicMock(side_effect=OSError("test_error")) - ): + with patch("pathlib.Path.write_bytes", side_effect=OSError("test_error")): await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "backup_failed" @@ -4047,35 +4045,35 @@ async def test_reconfigure_migrate_start_addon_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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 result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" result = await hass.config_entries.flow.async_configure( @@ -4090,13 +4088,13 @@ async def test_reconfigure_migrate_start_addon_failure( "core_zwave_js", AddonsOptions(config={"device": "/test"}) ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -4142,35 +4140,35 @@ async def test_reconfigure_migrate_restore_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is 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 result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" result = await hass.config_entries.flow.async_configure( @@ -4180,13 +4178,13 @@ async def test_reconfigure_migrate_restore_failure( }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" await hass.async_block_till_done() @@ -4195,13 +4193,13 @@ async def test_reconfigure_migrate_restore_failure( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "restore_failed" assert result["description_placeholders"]["file_path"] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" await hass.async_block_till_done() @@ -4210,7 +4208,7 @@ async def test_reconfigure_migrate_restore_failure( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "restore_failed" hass.config_entries.flow.async_abort(result["flow_id"]) @@ -4218,29 +4216,77 @@ async def test_reconfigure_migrate_restore_failure( assert len(hass.config_entries.flow.async_progress()) == 0 -async def test_get_driver_failure(hass: HomeAssistant, integration, client) -> None: - """Test get driver failure.""" +async def test_get_driver_failure_intent_migrate( + hass: HomeAssistant, + integration: MockConfigEntry, +) -> None: + """Test get driver failure in intent migrate step.""" entry = integration hass.config_entries.async_update_entry( integration, unique_id="1234", data={**integration.data, "use_addon": True} ) result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + await hass.config_entries.async_unload(integration.entry_id) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "config_entry_not_loaded" + + +async def test_get_driver_failure_instruct_unplug( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test get driver failure in instruct unplug step.""" + + 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 + ) + entry = integration + hass.config_entries.async_update_entry( + integration, unique_id="1234", data={**integration.data, "use_addon": True} + ) + result = await entry.start_reconfigure_flow(hass) + assert result["type"] is 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["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" - await hass.config_entries.async_unload(integration.entry_id) - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "config_entry_not_loaded" + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes") 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 + + await hass.config_entries.async_unload(integration.entry_id) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reset_failed" async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: @@ -4263,29 +4309,29 @@ async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> N result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reset_failed" @@ -4308,29 +4354,29 @@ async def test_choose_serial_port_usb_ports_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is 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["type"] is 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["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") 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 result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @@ -4339,7 +4385,7 @@ async def test_choose_serial_port_usb_ports_failure( side_effect=OSError("test_error"), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "usb_ports_failed" @@ -4350,14 +4396,14 @@ async def test_configure_addon_usb_ports_failure( entry = integration result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_reconfigure"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor_reconfigure" with patch( @@ -4367,5 +4413,5 @@ async def test_configure_addon_usb_ports_failure( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "usb_ports_failed" From abf6a809b890f1bdaadfb35b5a8334877386e960 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 19 May 2025 12:43:06 +0200 Subject: [PATCH 14/38] Fix Z-Wave unique id update during controller migration (#145185) --- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/api.py | 8 +- .../components/zwave_js/config_flow.py | 91 ++++- homeassistant/components/zwave_js/const.py | 2 +- tests/components/zwave_js/test_api.py | 4 +- tests/components/zwave_js/test_config_flow.py | 342 +++++++++++++++--- 6 files changed, 376 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 349baecc21d..6e76b2f89cf 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -105,6 +105,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, + DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_VALUE_UPDATED, LIB_LOGGER, @@ -135,7 +136,6 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_DRIVER_EVENTS = "driver_events" -DRIVER_READY_TIMEOUT = 60 CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index ddfd0cb003d..aff730a1eb2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -88,9 +88,9 @@ from .const import ( CONF_INSTALLER_MODE, DATA_CLIENT, DOMAIN, + DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, - RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( @@ -189,8 +189,6 @@ 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( @@ -2858,7 +2856,7 @@ async def websocket_hard_reset_controller( await driver.async_hard_reset() with suppress(TimeoutError): - async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() # When resetting the controller, the controller home id is also changed. @@ -3105,7 +3103,7 @@ async def websocket_restore_nvm( await controller.async_restore_nvm_base64(msg["data"]) with suppress(TimeoutError): - async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e52a5e784e8..e442fb59cfc 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -65,7 +65,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, - RESTORE_NVM_DRIVER_READY_TIMEOUT, + DRIVER_READY_TIMEOUT, ) from .helpers import CannotConnect, async_get_version_info @@ -776,17 +776,14 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) @callback - def _async_update_entry( - self, updates: dict[str, Any], *, schedule_reload: bool = True - ) -> None: + def _async_update_entry(self, updates: dict[str, Any]) -> None: """Update the config entry with new data.""" config_entry = self._reconfigure_config_entry assert config_entry is not None self.hass.config_entries.async_update_entry( config_entry, data=config_entry.data | updates ) - if schedule_reload: - self.hass.config_entries.async_schedule_reload(config_entry.entry_id) + self.hass.config_entries.async_schedule_reload(config_entry.entry_id) async def async_step_intent_reconfigure( self, user_input: dict[str, Any] | None = None @@ -896,15 +893,63 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): # Now that the old controller is gone, we can scan for serial ports again return await self.async_step_choose_serial_port() + try: + driver = self._get_driver() + except AbortFlow: + return self.async_abort(reason="config_entry_not_loaded") + + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + + unsubscribe = driver.once("driver ready", set_driver_ready) + # reset the old controller try: - await self._get_driver().async_hard_reset() - except (AbortFlow, FailedCommand) as err: + await driver.async_hard_reset() + except FailedCommand as err: + unsubscribe() _LOGGER.error("Failed to reset controller: %s", err) return self.async_abort(reason="reset_failed") + # Update the unique id of the config entry + # to the new home id, which requires waiting for the driver + # to be ready before getting the new home id. + # If the backup restore, done later in the flow, fails, + # the config entry unique id should be the new home id + # after the controller reset. + try: + async with asyncio.timeout(DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + except TimeoutError: + pass + finally: + unsubscribe() + config_entry = self._reconfigure_config_entry assert config_entry is not None + + try: + version_info = await async_get_version_info( + self.hass, config_entry.data[CONF_URL] + ) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded, if the backup restore + # also fails. + _LOGGER.debug( + "Failed to get server version, cannot update config entry " + "unique id with new home id, after controller reset" + ) + else: + self.hass.config_entries.async_update_entry( + config_entry, unique_id=str(version_info.home_id) + ) + # Unload the config entry before asking the user to unplug the controller. await self.hass.config_entries.async_unload(config_entry.entry_id) @@ -1154,14 +1199,17 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): assert ws_address is not None version_info = self.version_info assert version_info is not None + config_entry = self._reconfigure_config_entry + assert config_entry is not None # We need to wait for the config entry to be reloaded, # before restoring the backup. # We will do this in the restore nvm progress task, # to get a nicer user experience. - self._async_update_entry( - { - "unique_id": str(version_info.home_id), + self.hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, CONF_URL: ws_address, CONF_USB_PATH: self.usb_path, CONF_S0_LEGACY_KEY: self.s0_legacy_key, @@ -1173,8 +1221,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_USE_ADDON: True, CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, }, - schedule_reload=False, + unique_id=str(version_info.home_id), ) + return await self.async_step_restore_nvm() async def async_step_finish_addon_setup_reconfigure( @@ -1321,8 +1370,24 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): raise AbortFlow(f"Failed to restore network: {err}") from err else: with suppress(TimeoutError): - async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + try: + version_info = await async_get_version_info( + self.hass, config_entry.data[CONF_URL] + ) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded. + _LOGGER.error( + "Failed to get server version, cannot update config entry " + "unique id with new home id, after controller reset" + ) + else: + self.hass.config_entries.async_update_entry( + config_entry, unique_id=str(version_info.home_id) + ) await self.hass.config_entries.async_reload(config_entry.entry_id) finally: for unsub in unsubs: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 5792fca42a2..31cfb144e2a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -204,4 +204,4 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { # Other constants -RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 +DRIVER_READY_TIMEOUT = 60 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index a3f08513b70..19b24a1a7bb 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5175,7 +5175,7 @@ async def test_hard_reset_controller( 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", + "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", new=0, ): await ws_client.send_json_auto_id( @@ -5647,7 +5647,7 @@ async def test_restore_nvm( client.async_send_command.side_effect = async_send_command_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", new=0, ): # Send the subscription request diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index f14c94047b9..03f68d29c46 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -153,19 +153,6 @@ 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.""" @@ -861,8 +848,11 @@ async def test_usb_discovery_migration( restart_addon: AsyncMock, client: MagicMock, integration: MockConfigEntry, + get_server_version: AsyncMock, ) -> None: """Test usb discovery migration.""" + version_info = get_server_version.return_value + version_info.home_id = 4321 addon_options["device"] = "/dev/ttyUSB0" entry = integration assert client.connect.call_count == 1 @@ -887,6 +877,13 @@ async def test_usb_discovery_migration( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -938,6 +935,7 @@ async def test_usb_discovery_migration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.unique_id == "4321" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -952,6 +950,8 @@ async def test_usb_discovery_migration( assert restart_addon.call_args == call("core_zwave_js") + version_info.home_id = 5678 + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS @@ -970,9 +970,10 @@ async def test_usb_discovery_migration( 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 + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device + assert entry.data["use_addon"] is True + assert entry.unique_id == "5678" @pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") @@ -989,10 +990,9 @@ async def test_usb_discovery_migration( ] ], ) -async def test_usb_discovery_migration_driver_ready_timeout( +async def test_usb_discovery_migration_restore_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, @@ -1024,6 +1024,13 @@ async def test_usb_discovery_migration_driver_ready_timeout( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -1086,21 +1093,25 @@ async def test_usb_discovery_migration_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + with patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "restore_nvm" - assert client.connect.call_count == 2 + assert result["type"] is 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 + 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"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" @@ -3656,6 +3667,20 @@ async def test_reconfigure_migrate_low_sdk_version( ] ], ) +@pytest.mark.parametrize( + ( + "reset_server_version_side_effect", + "reset_unique_id", + "restore_server_version_side_effect", + "final_unique_id", + ), + [ + (None, "4321", None, "8765"), + (aiohttp.ClientError("Boom"), "1234", None, "8765"), + (None, "4321", aiohttp.ClientError("Boom"), "5678"), + (aiohttp.ClientError("Boom"), "1234", aiohttp.ClientError("Boom"), "5678"), + ], +) async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, client, @@ -3665,8 +3690,16 @@ async def test_reconfigure_migrate_with_addon( restart_addon, set_addon_options, get_addon_discovery_info, + get_server_version: AsyncMock, + reset_server_version_side_effect: Exception | None, + reset_unique_id: str, + restore_server_version_side_effect: Exception | None, + final_unique_id: str, ) -> None: """Test migration flow with add-on.""" + get_server_version.side_effect = reset_server_version_side_effect + version_info = get_server_version.return_value + version_info.home_id = 4321 entry = integration assert client.connect.call_count == 1 hass.config_entries.async_update_entry( @@ -3690,6 +3723,13 @@ async def test_reconfigure_migrate_with_addon( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -3740,6 +3780,175 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.unique_id == reset_unique_id + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + assert result["data_schema"].schema[CONF_USB_PATH] + + # Reset side effect before starting the add-on. + get_server_version.side_effect = None + version_info.home_id = 5678 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert result["type"] is 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 entry.unique_id == "5678" + get_server_version.side_effect = restore_server_version_side_effect + version_info.home_id = 8765 + + assert result["type"] is 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 entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == final_unique_id + + +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_reconfigure_migrate_reset_driver_ready_timeout( + hass: HomeAssistant, + client, + supervisor, + integration, + addon_running, + restart_addon, + set_addon_options, + get_addon_discovery_info, + get_server_version: AsyncMock, +) -> None: + """Test migration flow with driver ready timeout after controller reset.""" + version_info = get_server_version.return_value + version_info.home_id = 4321 + 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_reset_controller(): + await asyncio.sleep(0) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + + 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"] is 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"] is FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + with ( + patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ), + patch("pathlib.Path.write_bytes") as mock_file, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + 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"] is FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.unique_id == "4321" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3764,6 +3973,8 @@ async def test_reconfigure_migrate_with_addon( assert restart_addon.call_args == call("core_zwave_js") + version_info.home_id = 5678 + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS @@ -3782,9 +3993,10 @@ async def test_reconfigure_migrate_with_addon( 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 + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == "5678" @pytest.mark.parametrize( @@ -3800,13 +4012,12 @@ async def test_reconfigure_migrate_with_addon( ] ], ) -async def test_reconfigure_migrate_driver_ready_timeout( +async def test_reconfigure_migrate_restore_driver_ready_timeout( hass: HomeAssistant, client, supervisor, integration, addon_running, - driver_ready_timeout: None, restart_addon, set_addon_options, get_addon_discovery_info, @@ -3835,6 +4046,13 @@ async def test_reconfigure_migrate_driver_ready_timeout( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -3906,21 +4124,25 @@ async def test_reconfigure_migrate_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + with patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "restore_nvm" - assert client.connect.call_count == 2 + assert result["type"] is 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 + 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"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" @@ -4039,9 +4261,13 @@ async def test_reconfigure_migrate_start_addon_failure( client.driver.controller.async_backup_nvm_raw = AsyncMock( side_effect=mock_backup_nvm_raw ) - client.driver.controller.async_restore_nvm = AsyncMock( - side_effect=FailedCommand("test_error", "unknown_error") - ) + + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) result = await entry.start_reconfigure_flow(hass) @@ -4134,6 +4360,13 @@ async def test_reconfigure_migrate_restore_failure( client.driver.controller.async_backup_nvm_raw = AsyncMock( side_effect=mock_backup_nvm_raw ) + + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) client.driver.controller.async_restore_nvm = AsyncMock( side_effect=FailedCommand("test_error", "unknown_error") ) @@ -4286,7 +4519,7 @@ async def test_get_driver_failure_instruct_unplug( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reset_failed" + assert result["reason"] == "config_entry_not_loaded" async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: @@ -4352,6 +4585,13 @@ async def test_choose_serial_port_usb_ports_failure( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU From 64b7d778402a3b53cff6cb0d5eea4e21ecaa36b1 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 19 May 2025 13:20:02 +0200 Subject: [PATCH 15/38] Bump velbusaio to 2025.5.0 (#145198) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 2c05ae0301b..d64a1361987 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -14,7 +14,7 @@ "velbus-protocol" ], "quality_scale": "bronze", - "requirements": ["velbus-aio==2025.4.2"], + "requirements": ["velbus-aio==2025.5.0"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index da4c4e05caa..0059ecf4f50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3016,7 +3016,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2025.4.2 +velbus-aio==2025.5.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c43805aaeb..8fdd56eed76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2439,7 +2439,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2025.4.2 +velbus-aio==2025.5.0 # homeassistant.components.venstar venstarcolortouch==0.19 From 006f66a84194a50c58fdd8368319c8537fb6d199 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 15:16:12 +0300 Subject: [PATCH 16/38] Bump aiocomelit to 0.12.3 (#145209) --- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/comelit/test_climate.py | 2 +- tests/components/comelit/test_humidifier.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 58f347b4ba3..bea84c6b805 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aiocomelit"], "quality_scale": "bronze", - "requirements": ["aiocomelit==0.12.1"] + "requirements": ["aiocomelit==0.12.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0059ecf4f50..727a3305fcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -214,7 +214,7 @@ aiobafi6==0.9.0 aiobotocore==2.21.1 # homeassistant.components.comelit -aiocomelit==0.12.1 +aiocomelit==0.12.3 # homeassistant.components.dhcp aiodhcpwatcher==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fdd56eed76..d83fe8ca370 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,7 +202,7 @@ aiobafi6==0.9.0 aiobotocore==2.21.1 # homeassistant.components.comelit -aiocomelit==0.12.1 +aiocomelit==0.12.3 # homeassistant.components.dhcp aiodhcpwatcher==1.1.1 diff --git a/tests/components/comelit/test_climate.py b/tests/components/comelit/test_climate.py index 059d7d27d77..a380faeb5c0 100644 --- a/tests/components/comelit/test_climate.py +++ b/tests/components/comelit/test_climate.py @@ -84,7 +84,7 @@ async def test_climate_data_update( freezer: FrozenDateTimeFactory, mock_serial_bridge: AsyncMock, mock_serial_bridge_config_entry: MockConfigEntry, - val: list[Any, Any], + val: list[list[Any]], mode: HVACMode, temp: float, ) -> None: diff --git a/tests/components/comelit/test_humidifier.py b/tests/components/comelit/test_humidifier.py index 448453aadef..4b9d6324c6e 100644 --- a/tests/components/comelit/test_humidifier.py +++ b/tests/components/comelit/test_humidifier.py @@ -91,7 +91,7 @@ async def test_humidifier_data_update( freezer: FrozenDateTimeFactory, mock_serial_bridge: AsyncMock, mock_serial_bridge_config_entry: MockConfigEntry, - val: list[Any, Any], + val: list[list[Any]], mode: str, humidity: float, ) -> None: From 5094208db69d44cc511404d275aeeafb623f8ce7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 19 May 2025 16:05:48 +0200 Subject: [PATCH 17/38] Fix Z-Wave config entry unique id after NVM restore (#145221) * Fix Z-Wave config entry unique id after NVM restore * Remove stale comment --- homeassistant/components/zwave_js/api.py | 21 ++++++++++++ tests/components/zwave_js/test_api.py | 43 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index aff730a1eb2..add16e7bdc8 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -3105,6 +3105,27 @@ async def websocket_restore_nvm( with suppress(TimeoutError): async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + + # When restoring the NVM to the controller, the controller home id is also changed. + # The controller state in the client is stale after restoring the NVM, + # so get the new home id with a new client using the helper function. + # The client state will be refreshed by reloading the config entry, + # after the unique id of the config entry has been updated. + try: + version_info = await async_get_version_info(hass, entry.data[CONF_URL]) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded. + LOGGER.error( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller NVM restore" + ) + else: + hass.config_entries.async_update_entry( + entry, unique_id=str(version_info.home_id) + ) + await hass.config_entries.async_reload(entry.entry_id) connection.send_message( diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 19b24a1a7bb..9da283d60cf 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5551,8 +5551,12 @@ async def test_restore_nvm( integration, client, hass_ws_client: WebSocketGenerator, + get_server_version: AsyncMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the restore NVM websocket command.""" + entry = integration + assert entry.unique_id == "3245146787" ws_client = await hass_ws_client(hass) # Set up mocks for the controller events @@ -5632,6 +5636,45 @@ async def test_restore_nvm( }, require_schema=14, ) + assert entry.unique_id == "1234" + + client.async_send_command.reset_mock() + + # Test client connect error when getting the server version. + + get_server_version.side_effect = ClientError("Boom!") + + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": entry.entry_id, + "data": "dGVzdA==", # base64 encoded "test" + } + ) + + # Verify the finished event first + msg = await ws_client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["event"] == "finished" + + # Verify subscription success + msg = await ws_client.receive_json() + assert msg["type"] == "result" + assert msg["success"] is True + + assert client.async_send_command.call_count == 3 + assert client.async_send_command.call_args_list[0] == call( + { + "command": "controller.restore_nvm", + "nvmData": "dGVzdA==", + }, + require_schema=14, + ) + assert ( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller NVM restore" + ) in caplog.text client.async_send_command.reset_mock() From d91f01243c655e1afb5591be70bf88ba504a5ee4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 19 May 2025 21:12:51 +0200 Subject: [PATCH 18/38] Bump holidays to 0.73 (#145238) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 9809862cd52..bd6fd51e726 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.72", "babel==2.15.0"] + "requirements": ["holidays==0.73", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 542b68169a3..7a03133dd86 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.72"] + "requirements": ["holidays==0.73"] } diff --git a/requirements_all.txt b/requirements_all.txt index 727a3305fcd..32009fea864 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1158,7 +1158,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.72 +holidays==0.73 # homeassistant.components.frontend home-assistant-frontend==20250516.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d83fe8ca370..92a4c4421d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.72 +holidays==0.73 # homeassistant.components.frontend home-assistant-frontend==20250516.0 From d03af549d42a631a85dd3a0151da0d6384143637 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Tue, 20 May 2025 02:51:29 -0500 Subject: [PATCH 19/38] Bump pyaprilaire to 0.9.0 (#145260) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index b40460dd61b..6fe3beae3bc 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.8.1"] + "requirements": ["pyaprilaire==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 32009fea864..064aec41768 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1829,7 +1829,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.8.1 +pyaprilaire==0.9.0 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92a4c4421d9..a5807a42fe9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1510,7 +1510,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.8.1 +pyaprilaire==0.9.0 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From 9fc78ed4e2e44c644af303b2b979aaaf1150719e Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 23 May 2025 15:07:06 +0200 Subject: [PATCH 20/38] Add cloud as after_dependency to onedrive (#145301) --- homeassistant/components/onedrive/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/onedrive/manifest.json b/homeassistant/components/onedrive/manifest.json index c20a99c727e..a6b47b083dc 100644 --- a/homeassistant/components/onedrive/manifest.json +++ b/homeassistant/components/onedrive/manifest.json @@ -1,6 +1,7 @@ { "domain": "onedrive", "name": "OneDrive", + "after_dependencies": ["cloud"], "codeowners": ["@zweckj"], "config_flow": true, "dependencies": ["application_credentials"], From 777b04d7a54bc6acc1a3c47b0607e1338d5c4628 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 20 May 2025 19:42:10 +0200 Subject: [PATCH 21/38] Handle more exceptions in azure_storage (#145320) --- .../components/azure_storage/__init__.py | 4 +-- .../components/azure_storage/backup.py | 16 +++++++++- tests/components/azure_storage/test_backup.py | 30 ++++++++++++++----- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/azure_storage/__init__.py b/homeassistant/components/azure_storage/__init__.py index 00e419fd3c9..78d85dd6a59 100644 --- a/homeassistant/components/azure_storage/__init__.py +++ b/homeassistant/components/azure_storage/__init__.py @@ -2,8 +2,8 @@ from aiohttp import ClientTimeout from azure.core.exceptions import ( + AzureError, ClientAuthenticationError, - HttpResponseError, ResourceNotFoundError, ) from azure.core.pipeline.transport._aiohttp import ( @@ -70,7 +70,7 @@ async def async_setup_entry( translation_key="invalid_auth", translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]}, ) from err - except HttpResponseError as err: + except AzureError as err: raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="cannot_connect", diff --git a/homeassistant/components/azure_storage/backup.py b/homeassistant/components/azure_storage/backup.py index 4a9254213dc..54fd069a11f 100644 --- a/homeassistant/components/azure_storage/backup.py +++ b/homeassistant/components/azure_storage/backup.py @@ -8,7 +8,7 @@ import json import logging from typing import Any, Concatenate -from azure.core.exceptions import HttpResponseError +from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError from azure.storage.blob import BlobProperties from homeassistant.components.backup import ( @@ -80,6 +80,20 @@ def handle_backup_errors[_R, **P]( f"Error during backup operation in {func.__name__}:" f" Status {err.status_code}, message: {err.message}" ) from err + except ServiceRequestError as err: + raise BackupAgentError( + f"Timeout during backup operation in {func.__name__}" + ) from err + except AzureError as err: + _LOGGER.debug( + "Error during backup in %s: %s", + func.__name__, + err, + exc_info=True, + ) + raise BackupAgentError( + f"Error during backup operation in {func.__name__}: {err}" + ) from err return wrapper diff --git a/tests/components/azure_storage/test_backup.py b/tests/components/azure_storage/test_backup.py index 7c5912a4981..ebb491c2b7c 100644 --- a/tests/components/azure_storage/test_backup.py +++ b/tests/components/azure_storage/test_backup.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator from io import StringIO from unittest.mock import ANY, Mock, patch -from azure.core.exceptions import HttpResponseError +from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError from azure.storage.blob import BlobProperties import pytest @@ -276,14 +276,33 @@ async def test_agents_error_on_download_not_found( assert mock_client.download_blob.call_count == 0 +@pytest.mark.parametrize( + ("error", "message"), + [ + ( + HttpResponseError("http error"), + "Error during backup operation in async_delete_backup: Status None, message: http error", + ), + ( + ServiceRequestError("timeout"), + "Timeout during backup operation in async_delete_backup", + ), + ( + AzureError("generic error"), + "Error during backup operation in async_delete_backup: generic error", + ), + ], +) async def test_error_during_delete( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_client: MagicMock, mock_config_entry: MockConfigEntry, + error: Exception, + message: str, ) -> None: """Test the error wrapper.""" - mock_client.delete_blob.side_effect = HttpResponseError("Failed to delete backup") + mock_client.delete_blob.side_effect = error client = await hass_ws_client(hass) @@ -297,12 +316,7 @@ async def test_error_during_delete( assert response["success"] assert response["result"] == { - "agent_errors": { - f"{DOMAIN}.{mock_config_entry.entry_id}": ( - "Error during backup operation in async_delete_backup: " - "Status None, message: Failed to delete backup" - ) - } + "agent_errors": {f"{DOMAIN}.{mock_config_entry.entry_id}": message} } From f5cf64700a584b81ced02f370b7c69af38efd196 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 May 2025 10:22:47 +0200 Subject: [PATCH 22/38] Fix limit of shown backups on Synology DSM location (#145342) --- homeassistant/components/synology_dsm/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/backup.py b/homeassistant/components/synology_dsm/backup.py index 46e47ebde16..b3279db1cac 100644 --- a/homeassistant/components/synology_dsm/backup.py +++ b/homeassistant/components/synology_dsm/backup.py @@ -236,7 +236,7 @@ class SynologyDSMBackupAgent(BackupAgent): raise BackupAgentError("Failed to read meta data") from err try: - files = await self._file_station.get_files(path=self.path) + files = await self._file_station.get_files(path=self.path, limit=1000) except SynologyDSMAPIErrorException as err: raise BackupAgentError("Failed to list backups") from err From d9fe1edd82d31b8b9d9be956e4d212fad1a44262 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Tue, 20 May 2025 22:29:55 +0100 Subject: [PATCH 23/38] Add initial coordinator refresh for players in Squeezebox (#145347) * initial * add test for new player --- .../components/squeezebox/__init__.py | 1 + .../squeezebox/test_media_player.py | 34 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 78a97e38833..13c27f064f7 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -151,6 +151,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) - player_coordinator = SqueezeBoxPlayerUpdateCoordinator( hass, entry, player, lms.uuid ) + await player_coordinator.async_refresh() known_players.append(player.player_id) async_dispatcher_send( hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index f3292f1b469..b69f6cc9240 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -72,7 +72,12 @@ from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util.dt import utcnow -from .conftest import FAKE_VALID_ITEM_ID, TEST_MAC, TEST_VOLUME_STEP +from .conftest import ( + FAKE_VALID_ITEM_ID, + TEST_MAC, + TEST_VOLUME_STEP, + configure_squeezebox_media_player_platform, +) from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -100,6 +105,33 @@ async def test_entity_registry( await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +async def test_squeezebox_new_player_discovery( + hass: HomeAssistant, + config_entry: MockConfigEntry, + lms: MagicMock, + player_factory: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test discovery of a new squeezebox player.""" + # Initial setup with one player (from the 'lms' fixture) + await configure_squeezebox_media_player_platform(hass, config_entry, lms) + await hass.async_block_till_done(wait_background_tasks=True) + assert hass.states.get("media_player.test_player") is not None + assert hass.states.get("media_player.test_player_2") is None + + # Simulate a new player appearing + new_player_mock = player_factory(TEST_MAC[1]) + lms.async_get_players.return_value = [ + lms.async_get_players.return_value[0], + new_player_mock, + ] + + freezer.tick(timedelta(seconds=DISCOVERY_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get("media_player.test_player_2") is not None + + async def test_squeezebox_player_rediscovery( hass: HomeAssistant, configured_player: MagicMock, freezer: FrozenDateTimeFactory ) -> None: From 8c475787cc698d97c4b4755f35e492bc10bb8860 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 21 May 2025 14:19:37 +0200 Subject: [PATCH 24/38] Fix: Revert Ecovacs mower total_stats_area unit to square meters (#145380) --- homeassistant/components/ecovacs/sensor.py | 3 +-- tests/components/ecovacs/snapshots/test_sensor.ambr | 11 +---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index eab642119e4..3da1db23b24 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -98,9 +98,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( key="total_stats_area", translation_key="total_stats_area", device_class=SensorDeviceClass.AREA, - native_unit_of_measurement_fn=get_area_native_unit_of_measurement, + native_unit_of_measurement=UnitOfArea.SQUARE_METERS, state_class=SensorStateClass.TOTAL_INCREASING, - suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS, ), EcovacsSensorEntityDescription[TotalStatsEvent]( capability_fn=lambda caps: caps.stats.total, diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index c78df0e189a..468ff0a29f8 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -518,9 +518,6 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, @@ -546,7 +543,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0060', + 'state': '60', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:entity-registry] @@ -1269,9 +1266,6 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, @@ -1963,9 +1957,6 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, From 2403fff81ff85797dedf432039dd6357ac797278 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Wed, 21 May 2025 09:17:51 -0400 Subject: [PATCH 25/38] Bump pysqueezebox to v0.12.1 (#145384) --- homeassistant/components/squeezebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index e9b89291749..49e1da860df 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/squeezebox", "iot_class": "local_polling", "loggers": ["pysqueezebox"], - "requirements": ["pysqueezebox==0.12.0"] + "requirements": ["pysqueezebox==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 064aec41768..5a007f93567 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2356,7 +2356,7 @@ pyspcwebgw==0.7.0 pyspeex-noise==1.0.2 # homeassistant.components.squeezebox -pysqueezebox==0.12.0 +pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5807a42fe9..4ab664da765 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1929,7 +1929,7 @@ pyspcwebgw==0.7.0 pyspeex-noise==1.0.2 # homeassistant.components.squeezebox -pysqueezebox==0.12.0 +pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.1.0 From be0d4d926c1a1f17854c741a6423947bba903bc5 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 21 May 2025 17:11:19 +0200 Subject: [PATCH 26/38] OTBR: remove links to obsolete multiprotocol docs (#145394) --- homeassistant/components/otbr/util.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 30e456e11a8..363b1385327 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -19,9 +19,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon MultiprotocolAddonManager, get_multiprotocol_addon_manager, is_multiprotocol_url, - multi_pan_addon_using_device, ) -from homeassistant.components.homeassistant_yellow import RADIO_DEVICE as YELLOW_RADIO from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -34,10 +32,6 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -INFO_URL_SKY_CONNECT = ( - "https://skyconnect.home-assistant.io/multiprotocol-channel-missmatch" -) -INFO_URL_YELLOW = "https://yellow.home-assistant.io/multiprotocol-channel-missmatch" INSECURE_NETWORK_KEYS = ( # Thread web UI default @@ -208,16 +202,12 @@ async def _warn_on_channel_collision( delete_issue() return - yellow = await multi_pan_addon_using_device(hass, YELLOW_RADIO) - learn_more_url = INFO_URL_YELLOW if yellow else INFO_URL_SKY_CONNECT - ir.async_create_issue( hass, DOMAIN, f"otbr_zha_channel_collision_{otbrdata.entry_id}", is_fixable=False, is_persistent=False, - learn_more_url=learn_more_url, severity=ir.IssueSeverity.WARNING, translation_key="otbr_zha_channel_collision", translation_placeholders={ From e13b014b6faa6e733b483ea1a8fe783500bf0776 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 21 May 2025 22:02:14 +0200 Subject: [PATCH 27/38] Bump pylamarzocco to 2.0.4 (#145402) --- homeassistant/components/lamarzocco/manifest.json | 2 +- homeassistant/components/lamarzocco/update.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/test_update.py | 11 +++-------- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index d948d46ef1f..44ca31427c0 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.3"] + "requirements": ["pylamarzocco==2.0.4"] } diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index 632c66a8b66..33e64623256 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -4,7 +4,7 @@ import asyncio from dataclasses import dataclass from typing import Any -from pylamarzocco.const import FirmwareType, UpdateCommandStatus +from pylamarzocco.const import FirmwareType, UpdateStatus from pylamarzocco.exceptions import RequestNotSuccessful from homeassistant.components.update import ( @@ -125,7 +125,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): await self.coordinator.device.update_firmware() while ( update_progress := await self.coordinator.device.get_firmware() - ).command_status is UpdateCommandStatus.IN_PROGRESS: + ).command_status is UpdateStatus.IN_PROGRESS: if counter >= MAX_UPDATE_WAIT: _raise_timeout_error() self._attr_update_percentage = update_progress.progress_percentage diff --git a/requirements_all.txt b/requirements_all.txt index 5a007f93567..d8d415972dc 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.3 +pylamarzocco==2.0.4 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ab664da765..0513cf0037f 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.3 +pylamarzocco==2.0.4 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 3dbc5e98bee..3d02a0f38cf 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -3,12 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from pylamarzocco.const import ( - FirmwareType, - UpdateCommandStatus, - UpdateProgressInfo, - UpdateStatus, -) +from pylamarzocco.const import FirmwareType, UpdateProgressInfo, UpdateStatus from pylamarzocco.exceptions import RequestNotSuccessful from pylamarzocco.models import UpdateDetails import pytest @@ -61,7 +56,7 @@ async def test_update_process( mock_lamarzocco.get_firmware.side_effect = [ UpdateDetails( status=UpdateStatus.TO_UPDATE, - command_status=UpdateCommandStatus.IN_PROGRESS, + command_status=UpdateStatus.IN_PROGRESS, progress_info=UpdateProgressInfo.STARTING_PROCESS, progress_percentage=0, ), @@ -139,7 +134,7 @@ async def test_update_times_out( """Test error during update.""" mock_lamarzocco.get_firmware.return_value = UpdateDetails( status=UpdateStatus.TO_UPDATE, - command_status=UpdateCommandStatus.IN_PROGRESS, + command_status=UpdateStatus.IN_PROGRESS, progress_info=UpdateProgressInfo.STARTING_PROCESS, progress_percentage=0, ) From 63f69a9e3d60b4417334a9dc2769046a030a716b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 May 2025 22:22:33 +0200 Subject: [PATCH 28/38] Bump py-synologydsm-api to 2.7.2 (#145403) bump py-synologydsm-api to 2.7.2 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 3804de7f3f1..cd054c7eb74 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.7.1"], + "requirements": ["py-synologydsm-api==2.7.2"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index d8d415972dc..4b006f7b628 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1771,7 +1771,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.1 +py-synologydsm-api==2.7.2 # homeassistant.components.atome pyAtome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0513cf0037f..1e1fbb68d9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.1 +py-synologydsm-api==2.7.2 # homeassistant.components.hdmi_cec pyCEC==0.5.2 From c1bf596ebad9851532e0193fbab29fee8ac5e7b4 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 21 May 2025 21:12:43 +0200 Subject: [PATCH 29/38] Mark backflush binary sensor not supported for GS3 MP in lamarzocco (#145406) --- homeassistant/components/lamarzocco/binary_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 9bf04129095..c108bdb02d8 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import cast from pylamarzocco import LaMarzoccoMachine -from pylamarzocco.const import BackFlushStatus, MachineState, WidgetType +from pylamarzocco.const import BackFlushStatus, MachineState, ModelName, WidgetType from pylamarzocco.models import BackFlush, MachineStatus from homeassistant.components.binary_sensor import ( @@ -66,6 +66,9 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( is BackFlushStatus.REQUESTED ), entity_category=EntityCategory.DIAGNOSTIC, + supported_fn=lambda coordinator: ( + coordinator.device.dashboard.model_name != ModelName.GS3_MP + ), ), LaMarzoccoBinarySensorEntityDescription( key="websocket_connected", From fc8c403a3a30c673d5e347d10d2bd618b76904ca Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 17:26:04 +0200 Subject: [PATCH 30/38] Bump yt-dlp to 2025.05.22 (#145441) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index a6663b089ac..3ce80f497ef 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.03.31"], + "requirements": ["yt-dlp[default]==2025.05.22"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 4b006f7b628..954eb688c5d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3147,7 +3147,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.03.31 +yt-dlp[default]==2025.05.22 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e1fbb68d9e..085fadde26a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2549,7 +2549,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.03.31 +yt-dlp[default]==2025.05.22 # homeassistant.components.zamg zamg==0.3.6 From 1a227d6a10d6cb771b5a300ced1b7b7e212e9dab Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 23 May 2025 15:33:03 +0200 Subject: [PATCH 31/38] Reolink fix device migration (#145443) --- homeassistant/components/reolink/__init__.py | 156 +++++++++---------- tests/components/reolink/test_init.py | 51 ++++++ 2 files changed, 128 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 48b5dc1a3d6..57d41c20521 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -364,90 +364,88 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - for dev_id in device.identifiers: - (device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host) - if not device_uid: - continue + (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) - if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: - if ch is None: - new_device_id = f"{host.unique_id}" - else: - new_device_id = f"{host.unique_id}_{device_uid[1]}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", - device_uid, - new_device_id, - ) - new_identifiers = {(DOMAIN, new_device_id)} - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) + if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: + if ch is None: + new_device_id = f"{host.unique_id}" + else: + new_device_id = f"{host.unique_id}_{device_uid[1]}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) - if ch is None or is_chime: - continue # Do not consider the NVR itself or chimes - - # Check for wrongfully combined host with NVR entities in one device - # Can be removed in HA 2025.12 - if (DOMAIN, host.unique_id) in device.identifiers: - new_identifiers = device.identifiers.copy() - for old_id in device.identifiers: - if old_id[0] == DOMAIN and old_id[1] != host.unique_id: - new_identifiers.remove(old_id) - _LOGGER.debug( - "Updating Reolink device identifiers from %s to %s", - device.identifiers, - new_identifiers, - ) - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - break - - # Check for wrongfully added MAC of the NVR/Hub to the camera - # Can be removed in HA 2025.12 - host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) - if host_connnection in device.connections: - new_connections = device.connections.copy() - new_connections.remove(host_connnection) - _LOGGER.debug( - "Updating Reolink device connections from %s to %s", - device.connections, - new_connections, - ) - device_reg.async_update_device( - device.id, new_connections=new_connections - ) - - ch_device_ids[device.id] = ch - if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid( - ch + # Check for wrongfully combined entities in one device + # Can be removed in HA 2025.12 + new_identifiers = device.identifiers.copy() + remove_ids = False + if (DOMAIN, host.unique_id) in device.identifiers: + remove_ids = True # NVR/Hub in identifiers, keep that one, remove others + for old_id in device.identifiers: + (old_device_uid, old_ch, old_is_chime) = get_device_uid_and_ch(old_id, host) + if ( + not old_device_uid + or old_device_uid[0] != host.unique_id + or old_id[1] == host.unique_id ): - if host.api.supported(None, "UID"): - new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" - else: - new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", - device_uid, + continue + if remove_ids: + new_identifiers.remove(old_id) + remove_ids = True # after the first identifier, remove the others + if new_identifiers != device.identifiers: + _LOGGER.debug( + "Updating Reolink device identifiers from %s to %s", + device.identifiers, + new_identifiers, + ) + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + break + + if ch is None or is_chime: + continue # Do not consider the NVR itself or chimes + + # Check for wrongfully added MAC of the NVR/Hub to the camera + # Can be removed in HA 2025.12 + host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) + if host_connnection in device.connections: + new_connections = device.connections.copy() + new_connections.remove(host_connnection) + _LOGGER.debug( + "Updating Reolink device connections from %s to %s", + device.connections, + new_connections, + ) + device_reg.async_update_device(device.id, new_connections=new_connections) + + ch_device_ids[device.id] = ch + if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): + if host.api.supported(None, "UID"): + new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" + else: + new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + existing_device = device_reg.async_get_device(identifiers=new_identifiers) + if existing_device is None: + device_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + else: + _LOGGER.warning( + "Reolink device with uid %s already exists, " + "removing device with uid %s", new_device_id, + device_uid, ) - new_identifiers = {(DOMAIN, new_device_id)} - existing_device = device_reg.async_get_device( - identifiers=new_identifiers - ) - if existing_device is None: - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - else: - _LOGGER.warning( - "Reolink device with uid %s already exists, " - "removing device with uid %s", - new_device_id, - device_uid, - ) - device_reg.async_remove_device(device.id) + device_reg.async_remove_device(device.id) entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index f2ae22913ad..3551632903f 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -724,6 +724,57 @@ async def test_cleanup_combined_with_NVR( reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM +async def test_cleanup_hub_and_direct_connection( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup of the device registry if IPC camera device was connected directly and through the hub/NVR.""" + reolink_connect.channels = [0] + entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + start_identifiers = { + (DOMAIN, dev_id), # IPC camera through hub + (DOMAIN, TEST_UID_CAM), # directly connected IPC camera + ("OTHER_INTEGRATION", "SOME_ID"), + } + + dev_entry = device_registry.async_get_or_create( + identifiers=start_identifiers, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC_CAM)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=entity_id, + config_entry=config_entry, + suggested_object_id=entity_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From d3b3839ffad804d4f074cbef32de1fbec44d17b6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 17:26:59 +0200 Subject: [PATCH 32/38] Bump pysmartthings to 3.2.3 (#145444) --- 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 f72405dae20..180d4eebed1 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.2"] + "requirements": ["pysmartthings==3.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 954eb688c5d..dfdc333a679 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.2 +pysmartthings==3.2.3 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 085fadde26a..cbd03b69eaf 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.2 +pysmartthings==3.2.3 # homeassistant.components.smarty pysmarty2==0.10.2 From f867a0af24067498adda7fc435e3ee905735a856 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 22 May 2025 13:22:20 -0700 Subject: [PATCH 33/38] Bump opower to 0.12.1 (#145464) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index a09405f1ca8..beaf63ad59d 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.12.0"] + "requirements": ["opower==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index dfdc333a679..e1b37f1a291 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1614,7 +1614,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.0 +opower==0.12.1 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cbd03b69eaf..43463a587fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.0 +opower==0.12.1 # homeassistant.components.oralb oralb-ble==0.17.6 From 97004e13cb9ac4ba64f636362291729de1da233b Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 22 May 2025 22:02:30 -0700 Subject: [PATCH 34/38] Make Gemma models work in Google AI (#145479) * Make Gemma models work in Google AI * move one line to be improve readability --- .../google_generative_ai_conversation/config_flow.py | 6 +++--- .../google_generative_ai_conversation/conversation.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index 551f9b0c9de..ae0f09b1037 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -254,11 +254,11 @@ async def google_generative_ai_config_option_schema( ) for api_model in sorted(api_models, key=lambda x: x.display_name or "") if ( - api_model.name != "models/gemini-1.0-pro" # duplicate of gemini-pro - and api_model.display_name + api_model.display_name and api_model.name - and api_model.supported_actions + and "tts" not in api_model.name and "vision" not in api_model.name + and api_model.supported_actions and "generateContent" in api_model.supported_actions ) ] diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 73a82b98664..c642bfd94e6 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -319,11 +319,10 @@ class GoogleGenerativeAIConversationEntity( tools.append(Tool(google_search=GoogleSearch())) model_name = self.entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL) - # Gemini 1.0 doesn't support system_instruction while 1.5 does. - # Assume future versions will support it (if not, the request fails with a - # clear message at which point we can fix). + # Avoid INVALID_ARGUMENT Developer instruction is not enabled for supports_system_instruction = ( - "gemini-1.0" not in model_name and "gemini-pro" not in model_name + "gemma" not in model_name + and "gemini-2.0-flash-preview-image-generation" not in model_name ) prompt_content = cast( From f0501f917b9624b23f9f676e096b0269a3013e5a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 22 May 2025 22:01:48 -0700 Subject: [PATCH 35/38] Fix strings related to Google search tool in Google AI (#145480) --- .../components/google_generative_ai_conversation/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index 2697f30eda0..a57e2f78f53 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -41,12 +41,12 @@ }, "data_description": { "prompt": "Instruct how the LLM should respond. This can be a template.", - "enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"." + "enable_google_search_tool": "Only works if there is nothing selected in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"." } } }, "error": { - "invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"." + "invalid_google_search_option": "Google Search can only be enabled if nothing is selected in the \"Control Home Assistant\" setting." } }, "services": { From 0aef8b58d8edfea4aef28fe004fe88a9785e9611 Mon Sep 17 00:00:00 2001 From: rappenze Date: Fri, 23 May 2025 12:05:13 +0200 Subject: [PATCH 36/38] Bump pyfibaro to 0.8.3 (#145488) --- homeassistant/components/fibaro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index cd4d1de838c..563ad8e08ce 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.8.2"] + "requirements": ["pyfibaro==0.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index e1b37f1a291..c9ee4db6ac8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.8.2 +pyfibaro==0.8.3 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43463a587fc..fd18e77ca72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1612,7 +1612,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.8.2 +pyfibaro==0.8.3 # homeassistant.components.fido pyfido==2.1.2 From e540247c145ebc560ead67e2656017e5b30e4bb4 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 23 May 2025 14:54:18 +0200 Subject: [PATCH 37/38] Bump deebot-client to 13.2.1 (#145492) --- 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 b1674e123fa..c2daf3a7e90 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.2.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==13.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c9ee4db6ac8..f3451ca4a8b 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.2.0 +deebot-client==13.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd18e77ca72..662353e1e57 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.2.0 +deebot-client==13.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 9a183bc16a0bd9b5861044585ca4e8070de3246d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 May 2025 14:03:47 +0000 Subject: [PATCH 38/38] Bump version to 2025.5.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9e3149fd89a..931f00803a6 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 = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index cc11f10d3cc..2cb43bc3e49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.2" +version = "2025.5.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."