From 619fdea5dfeeccd08eff78681913721748623140 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 7 May 2025 18:50:45 +0200 Subject: [PATCH 001/109] Fix Z-Wave restore nvm command to wait for driver ready (#144413) --- homeassistant/components/zwave_js/api.py | 15 ++ .../components/zwave_js/config_flow.py | 2 +- homeassistant/components/zwave_js/const.py | 4 + tests/components/zwave_js/test_api.py | 152 +++++++++++++----- 4 files changed, 129 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index aa2219031d2..f4397737234 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -88,6 +88,7 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( @@ -3063,14 +3064,28 @@ async def websocket_restore_nvm( ) ) + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + # Set up subscription for progress events connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), + driver.once("driver ready", set_driver_ready), ] await controller.async_restore_nvm_base64(msg["data"]) + + with suppress(TimeoutError): + async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + await hass.config_entries.async_reload(entry.entry_id) + connection.send_message( websocket_api.event_message( msg[ID], diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 84717047fdd..407af9e902b 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -67,6 +67,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, + RESTORE_NVM_DRIVER_READY_TIMEOUT, ) _LOGGER = logging.getLogger(__name__) @@ -78,7 +79,6 @@ ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" -RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 ADDON_LOG_LEVELS = { diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 16cf6f748bb..5792fca42a2 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -201,3 +201,7 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE, WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION, } + +# Other constants + +RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 2e3d8fd290a..c6ce3d9ac1b 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5518,10 +5518,98 @@ async def test_restore_nvm( # Set up mocks for the controller events controller = client.driver.controller - # Test restore success - with patch.object( - controller, "async_restore_nvm_base64", return_value=None - ) as mock_restore: + async def async_send_command_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + return {} + + client.async_send_command.side_effect = async_send_command_driver_ready + + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": integration.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 + + # Simulate progress events + event = Event( + "nvm restore progress", + { + "source": "controller", + "event": "nvm restore progress", + "bytesWritten": 25, + "total": 100, + }, + ) + controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 25 + assert msg["event"]["total"] == 100 + + event = Event( + "nvm restore progress", + { + "source": "controller", + "event": "nvm restore progress", + "bytesWritten": 50, + "total": 100, + }, + ) + controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 50 + assert msg["event"]["total"] == 100 + + await hass.async_block_till_done() + + # Verify the restore was called + # The first call is the relevant one for nvm restore. + 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, + ) + + client.async_send_command.reset_mock() + + # Test sending command with driver not ready and timeout. + + async def async_send_command_no_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + return {} + + client.async_send_command.side_effect = async_send_command_no_driver_ready + + with patch( + "homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT", + new=0, + ): # Send the subscription request await ws_client.send_json_auto_id( { @@ -5533,6 +5621,7 @@ async def test_restore_nvm( # Verify the finished event first msg = await ws_client.receive_json() + assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5541,48 +5630,25 @@ async def test_restore_nvm( assert msg["type"] == "result" assert msg["success"] is True - # Simulate progress events - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 25, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 25 - assert msg["event"]["total"] == 100 - - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 50, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 50 - assert msg["event"]["total"] == 100 - - # Wait for the restore to complete await hass.async_block_till_done() - # Verify the restore was called - assert mock_restore.called + # Verify the restore was called + # The first call is the relevant one for nvm restore. + 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, + ) + + client.async_send_command.reset_mock() # Test restore failure - with patch.object( - controller, - "async_restore_nvm_base64", - side_effect=FailedCommand("failed_command", "Restore failed"), + with patch( + f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): # Send the subscription request await ws_client.send_json_auto_id( @@ -5596,7 +5662,7 @@ async def test_restore_nvm( # Verify error response msg = await ws_client.receive_json() assert not msg["success"] - assert msg["error"]["code"] == "Restore failed" + assert msg["error"]["code"] == "zwave_error" # Test entry_id not found await ws_client.send_json_auto_id( From 6eb2d1aa7c6758ae2d38cbe0b33eaccb355abd3c Mon Sep 17 00:00:00 2001 From: Tamer Wahba Date: Thu, 8 May 2025 18:41:14 -0400 Subject: [PATCH 002/109] fix homekit air purifier temperature sensor to convert unit (#144435) --- .../components/homekit/type_air_purifiers.py | 22 ++++++++++++++++--- .../homekit/test_type_air_purifiers.py | 18 +++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_air_purifiers.py b/homeassistant/components/homekit/type_air_purifiers.py index 25d305a0aa9..feb75f4a856 100644 --- a/homeassistant/components/homekit/type_air_purifiers.py +++ b/homeassistant/components/homekit/type_air_purifiers.py @@ -8,7 +8,13 @@ from pyhap.const import CATEGORY_AIR_PURIFIER from pyhap.service import Service from pyhap.util import callback as pyhap_callback -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfTemperature, +) from homeassistant.core import ( Event, EventStateChangedData, @@ -43,7 +49,12 @@ from .const import ( THRESHOLD_FILTER_CHANGE_NEEDED, ) from .type_fans import ATTR_PRESET_MODE, CHAR_ROTATION_SPEED, Fan -from .util import cleanup_name_for_homekit, convert_to_float, density_to_air_quality +from .util import ( + cleanup_name_for_homekit, + convert_to_float, + density_to_air_quality, + temperature_to_homekit, +) _LOGGER = logging.getLogger(__name__) @@ -345,8 +356,13 @@ class AirPurifier(Fan): ): return + unit = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature.CELSIUS + ) + current_temperature = temperature_to_homekit(current_temperature, unit) + _LOGGER.debug( - "%s: Linked temperature sensor %s changed to %d", + "%s: Linked temperature sensor %s changed to %d °C", self.entity_id, self.linked_temperature_sensor, current_temperature, diff --git a/tests/components/homekit/test_type_air_purifiers.py b/tests/components/homekit/test_type_air_purifiers.py index 90b0e0047de..acc7838652d 100644 --- a/tests/components/homekit/test_type_air_purifiers.py +++ b/tests/components/homekit/test_type_air_purifiers.py @@ -34,9 +34,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant @@ -437,6 +439,22 @@ async def test_expose_linked_sensors( assert acc.char_air_quality.value == 1 assert len(broker.mock_calls) == 0 + # Updated temperature with different unit should reflect in HomeKit + broker = MagicMock() + acc.char_current_temperature.broker = broker + hass.states.async_set( + temperature_entity_id, + 60, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, + }, + ) + await hass.async_block_till_done() + assert acc.char_current_temperature.value == 15.6 + assert len(broker.mock_calls) == 2 + broker.reset_mock() + # Updated temperature should reflect in HomeKit broker = MagicMock() acc.char_current_temperature.broker = broker From 054c7a0adcfb5f7ccaeaa4487402e0bb1878db3e Mon Sep 17 00:00:00 2001 From: DukeChocula <79062549+DukeChocula@users.noreply.github.com> Date: Fri, 9 May 2025 04:49:26 -0500 Subject: [PATCH 003/109] Add LAP-V102S-AUSR to VeSync (#144437) Update const.py Added LAP-V102S-AUSR to Vital 100S --- homeassistant/components/vesync/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 4e39fe40f2d..ff55bcf2e37 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -97,6 +97,7 @@ SKU_TO_BASE_DEVICE = { "LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S "LAP-V102S-WEU": "Vital100S", # Alt ID Model Vital100S "LAP-V102S-WUK": "Vital100S", # Alt ID Model Vital100S + "LAP-V102S-AUSR": "Vital100S", # Alt ID Model Vital100S "EverestAir": "EverestAir", "LAP-EL551S-AUS": "EverestAir", # Alt ID Model EverestAir "LAP-EL551S-AEUR": "EverestAir", # Alt ID Model EverestAir From a793503c8a25470addf5163ce56c6864c0fcd304 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 8 May 2025 10:13:42 +0200 Subject: [PATCH 004/109] Bump pylamarzocco to 2.0.1 (#144454) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 572f70bc455..fb6a3660c66 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.0"] + "requirements": ["pylamarzocco==2.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index da5bdddd5c2..ff231fb44ef 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.0 +pylamarzocco==2.0.1 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7f6f484a70..b0d5fffe420 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.0 +pylamarzocco==2.0.1 # homeassistant.components.lastfm pylast==5.1.0 From e5c56629e25d32db2032f5b22e60813cf4ba9938 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 May 2025 11:59:20 +0200 Subject: [PATCH 005/109] Fix Z-Wave reset accumulated values button entity category (#144459) --- homeassistant/components/zwave_js/discovery.py | 2 +- tests/components/zwave_js/test_discovery.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 5c79c668afc..b46735e4040 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1204,7 +1204,7 @@ DISCOVERY_SCHEMAS = [ property={RESET_METER_PROPERTY}, type={ValueType.BOOLEAN}, ), - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=EntityCategory.CONFIG, ), ZWaveDiscoverySchema( platform=Platform.BINARY_SENSOR, diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 0be0cca78c8..7ef5f0e480f 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -431,10 +431,11 @@ async def test_rediscovery( async def test_aeotec_smart_switch_7( hass: HomeAssistant, + entity_registry: er.EntityRegistry, aeotec_smart_switch_7: Node, integration: MockConfigEntry, ) -> None: - """Test that Smart Switch 7 has a light and a switch entity.""" + """Test Smart Switch 7 discovery.""" state = hass.states.get("light.smart_switch_7") assert state assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ @@ -443,3 +444,9 @@ async def test_aeotec_smart_switch_7( state = hass.states.get("switch.smart_switch_7") assert state + + state = hass.states.get("button.smart_switch_7_reset_accumulated_values") + assert state + entity_entry = entity_registry.async_get(state.entity_id) + assert entity_entry + assert entity_entry.entity_category is EntityCategory.CONFIG From 23244fb79fcb67904d55cfebfe3873495d2b4842 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 8 May 2025 14:28:56 +0200 Subject: [PATCH 006/109] Fix point import error (#144462) * fix import error * fix failing tests * Apply suggestions from code review --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/point/coordinator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/point/coordinator.py b/homeassistant/components/point/coordinator.py index c0cb4e27646..93bd74955ea 100644 --- a/homeassistant/components/point/coordinator.py +++ b/homeassistant/components/point/coordinator.py @@ -6,7 +6,6 @@ import logging from typing import Any from pypoint import PointSession -from tempora.utc import fromtimestamp from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -62,7 +61,9 @@ class PointDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]] or device.device_id not in self.device_updates or self.device_updates[device.device_id] < last_updated ): - self.device_updates[device.device_id] = last_updated or fromtimestamp(0) + self.device_updates[device.device_id] = ( + last_updated or datetime.fromtimestamp(0) + ) self.data[device.device_id] = { k: await device.sensor(k) for k in ("temperature", "humidity", "sound_pressure") From a8beec26916166e760e500decb1fabf4deaf811c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 9 May 2025 10:39:00 +0200 Subject: [PATCH 007/109] Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue (#144463) --- homeassistant/components/fronius/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 4ba893df85c..8a3d1ebf04c 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -45,7 +45,15 @@ type FroniusConfigEntry = ConfigEntry[FroniusSolarNet] async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool: """Set up fronius from a config entry.""" host = entry.data[CONF_HOST] - fronius = Fronius(async_get_clientsession(hass), host) + fronius = Fronius( + async_get_clientsession( + hass, + # Fronius Gen24 firmware 1.35.4-1 redirects to HTTPS with self-signed + # certificate. See https://github.com/home-assistant/core/issues/138881 + verify_ssl=False, + ), + host, + ) solar_net = FroniusSolarNet(hass, entry, fronius) await solar_net.init_devices() From 30f7e9b441a39656ad8cbeb612b8379718dd0c69 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 May 2025 22:30:35 +0200 Subject: [PATCH 008/109] Don't encrypt or decrypt unknown files in backup archives (#144495) --- homeassistant/components/backup/http.py | 16 ++- homeassistant/components/backup/util.py | 92 +++++++++++++----- .../backup/fixtures/test_backups/c0cb53bd.tar | Bin 10240 -> 10240 bytes .../test_backups/c0cb53bd.tar.decrypted | Bin 10240 -> 10240 bytes tests/components/backup/test_http.py | 4 +- tests/components/backup/test_util.py | 64 ++++++++---- 6 files changed, 129 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py index 8f241e6363d..11d8199bdc5 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -22,7 +22,7 @@ from . import util from .agent import BackupAgent from .const import DATA_MANAGER from .manager import BackupManager -from .models import BackupNotFound +from .models import AgentBackup, BackupNotFound @callback @@ -85,7 +85,15 @@ class DownloadBackupView(HomeAssistantView): request, headers, backup_id, agent_id, agent, manager ) return await self._send_backup_with_password( - hass, request, headers, backup_id, agent_id, password, agent, manager + hass, + backup, + request, + headers, + backup_id, + agent_id, + password, + agent, + manager, ) except BackupNotFound: return Response(status=HTTPStatus.NOT_FOUND) @@ -116,6 +124,7 @@ class DownloadBackupView(HomeAssistantView): async def _send_backup_with_password( self, hass: HomeAssistant, + backup: AgentBackup, request: Request, headers: dict[istr, str], backup_id: str, @@ -144,7 +153,8 @@ class DownloadBackupView(HomeAssistantView): stream = util.AsyncIteratorWriter(hass) worker = threading.Thread( - target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []] + target=util.decrypt_backup, + args=[backup, reader, stream, password, on_done, 0, []], ) try: worker.start() diff --git a/homeassistant/components/backup/util.py b/homeassistant/components/backup/util.py index bd77880738e..8112faf4459 100644 --- a/homeassistant/components/backup/util.py +++ b/homeassistant/components/backup/util.py @@ -295,13 +295,26 @@ def validate_password_stream( raise BackupEmpty +def _get_expected_archives(backup: AgentBackup) -> set[str]: + """Get the expected archives in the backup.""" + expected_archives = set() + if backup.homeassistant_included: + expected_archives.add("homeassistant") + for addon in backup.addons: + expected_archives.add(addon.slug) + for folder in backup.folders: + expected_archives.add(folder.value) + return expected_archives + + def decrypt_backup( + backup: AgentBackup, input_stream: IO[bytes], output_stream: IO[bytes], password: str | None, on_done: Callable[[Exception | None], None], minimum_size: int, - nonces: list[bytes], + nonces: NonceGenerator, ) -> None: """Decrypt a backup.""" error: Exception | None = None @@ -315,7 +328,7 @@ def decrypt_backup( fileobj=output_stream, mode="w|", bufsize=BUF_SIZE ) as output_tar, ): - _decrypt_backup(input_tar, output_tar, password) + _decrypt_backup(backup, input_tar, output_tar, password) except (DecryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error decrypting backup: %s", err) error = err @@ -333,15 +346,18 @@ def decrypt_backup( def _decrypt_backup( + backup: AgentBackup, input_tar: tarfile.TarFile, output_tar: tarfile.TarFile, password: str | None, ) -> None: """Decrypt a backup.""" + expected_archives = _get_expected_archives(backup) for obj in input_tar: # We compare with PurePath to avoid issues with different path separators, # for example when backup.json is added as "./backup.json" - if PurePath(obj.name) == PurePath("backup.json"): + object_path = PurePath(obj.name) + if object_path == PurePath("backup.json"): # Rewrite the backup.json file to indicate that the backup is decrypted if not (reader := input_tar.extractfile(obj)): raise DecryptError @@ -352,7 +368,13 @@ def _decrypt_backup( metadata_obj.size = len(updated_metadata_b) output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b)) continue - if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): + prefix, _, suffix = object_path.name.partition(".") + if suffix not in ("tar", "tgz", "tar.gz"): + LOGGER.debug("Unknown file %s will not be decrypted", obj.name) + output_tar.addfile(obj, input_tar.extractfile(obj)) + continue + if prefix not in expected_archives: + LOGGER.debug("Unknown inner tar file %s will not be decrypted", obj.name) output_tar.addfile(obj, input_tar.extractfile(obj)) continue istf = SecureTarFile( @@ -371,12 +393,13 @@ def _decrypt_backup( def encrypt_backup( + backup: AgentBackup, input_stream: IO[bytes], output_stream: IO[bytes], password: str | None, on_done: Callable[[Exception | None], None], minimum_size: int, - nonces: list[bytes], + nonces: NonceGenerator, ) -> None: """Encrypt a backup.""" error: Exception | None = None @@ -390,7 +413,7 @@ def encrypt_backup( fileobj=output_stream, mode="w|", bufsize=BUF_SIZE ) as output_tar, ): - _encrypt_backup(input_tar, output_tar, password, nonces) + _encrypt_backup(backup, input_tar, output_tar, password, nonces) except (EncryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error encrypting backup: %s", err) error = err @@ -408,17 +431,20 @@ def encrypt_backup( def _encrypt_backup( + backup: AgentBackup, input_tar: tarfile.TarFile, output_tar: tarfile.TarFile, password: str | None, - nonces: list[bytes], + nonces: NonceGenerator, ) -> None: """Encrypt a backup.""" inner_tar_idx = 0 + expected_archives = _get_expected_archives(backup) for obj in input_tar: # We compare with PurePath to avoid issues with different path separators, # for example when backup.json is added as "./backup.json" - if PurePath(obj.name) == PurePath("backup.json"): + object_path = PurePath(obj.name) + if object_path == PurePath("backup.json"): # Rewrite the backup.json file to indicate that the backup is encrypted if not (reader := input_tar.extractfile(obj)): raise EncryptError @@ -429,16 +455,21 @@ def _encrypt_backup( metadata_obj.size = len(updated_metadata_b) output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b)) continue - if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): + prefix, _, suffix = object_path.name.partition(".") + if suffix not in ("tar", "tgz", "tar.gz"): + LOGGER.debug("Unknown file %s will not be encrypted", obj.name) output_tar.addfile(obj, input_tar.extractfile(obj)) continue + if prefix not in expected_archives: + LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name) + continue istf = SecureTarFile( None, # Not used gzip=False, key=password_to_key(password) if password is not None else None, mode="r", fileobj=input_tar.extractfile(obj), - nonce=nonces[inner_tar_idx], + nonce=nonces.get(inner_tar_idx), ) inner_tar_idx += 1 with istf.encrypt(obj) as encrypted: @@ -456,17 +487,33 @@ class _CipherWorkerStatus: writer: AsyncIteratorWriter +class NonceGenerator: + """Generate nonces for encryption.""" + + def __init__(self) -> None: + """Initialize the generator.""" + self._nonces: dict[int, bytes] = {} + + def get(self, index: int) -> bytes: + """Get a nonce for the given index.""" + if index not in self._nonces: + # Generate a new nonce for the given index + self._nonces[index] = os.urandom(16) + return self._nonces[index] + + class _CipherBackupStreamer: """Encrypt or decrypt a backup.""" _cipher_func: Callable[ [ + AgentBackup, IO[bytes], IO[bytes], str | None, Callable[[Exception | None], None], int, - list[bytes], + NonceGenerator, ], None, ] @@ -484,7 +531,7 @@ class _CipherBackupStreamer: self._hass = hass self._open_stream = open_stream self._password = password - self._nonces: list[bytes] = [] + self._nonces = NonceGenerator() def size(self) -> int: """Return the maximum size of the decrypted or encrypted backup.""" @@ -508,7 +555,15 @@ class _CipherBackupStreamer: writer = AsyncIteratorWriter(self._hass) worker = threading.Thread( target=self._cipher_func, - args=[reader, writer, self._password, on_done, self.size(), self._nonces], + args=[ + self._backup, + reader, + writer, + self._password, + on_done, + self.size(), + self._nonces, + ], ) worker_status = _CipherWorkerStatus( done=asyncio.Event(), reader=reader, thread=worker, writer=writer @@ -538,17 +593,6 @@ class DecryptedBackupStreamer(_CipherBackupStreamer): class EncryptedBackupStreamer(_CipherBackupStreamer): """Encrypt a backup.""" - def __init__( - self, - hass: HomeAssistant, - backup: AgentBackup, - open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]], - password: str | None, - ) -> None: - """Initialize.""" - super().__init__(hass, backup, open_stream, password) - self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())] - _cipher_func = staticmethod(encrypt_backup) def backup(self) -> AgentBackup: diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar index f3b2845d5eb19b9708ae6d4fd68f7d220fb39c45..29e61d5e4c11683b126dcc08b0736dca19eda9f4 100644 GIT binary patch delta 283 zcmZn&Xb70lB57`F!eD>^3w}8B;bhGKMkL>nJECrljQO6)RaOL{}>n z=ai-cSxU+IMX82LK*_ws+*FW&Gf+SQEK-(QRGgWg2NE>UGXSY6&a48d0rF~f6j04D z!Y~6Y0yjepn<25a69q*Uv9O3_7`dq6bzW0!ePX2WMFIr l_X#I6JBJ}m!NQ2iDFUV}hHzOyX7w}8B;bhGKMiR)&iL;7O}8Q5@4Ck%Mm_N rfpc<-fC;MsLKP=7`(_pi8K%t&LQhy3jUlQyfT~PcjNwK}{^tV#9Tpno diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted index c97533fc1afb35fafb7be57651fc72e4069f44b3..386ea021247a5b5f9658a1e2009e956f73dcca9d 100644 GIT binary patch delta 282 zcmZn&Xb70lB57`F%3y#13w}8B;bhGKMqN>nJECrljQO6)RaOL{}>n z=ai-cSxU+IMX82LK*_ws+*FW&Gf+SQEK-(QRGgWg2NE>UGXSY6&a48d0rF~f6j04D z!Y~6Y0yjepn<25a69t7Av9O3_7`dq6bzW0!ePX2WMFIz k_X#I6JBJ}m!NQ2iDFUV}MsQg{X7w}8B;bhGKMoT)&iN!7qPHR5@4Ck%Mm_N qfpc<-fC;MsLKP=7`(_r2Kg^p%SXda1A&NMFicDEd;3i4_=K}x=rW(lr diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index 92bf454095e..b3845b1209a 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -177,7 +177,7 @@ async def _test_downloading_encrypted_backup( enc_metadata = json.loads(outer_tar.extractfile("./backup.json").read()) assert enc_metadata["protected"] is True with ( - outer_tar.extractfile("core.tar.gz") as inner_tar_file, + outer_tar.extractfile("homeassistant.tar.gz") as inner_tar_file, pytest.raises(tarfile.ReadError, match="file could not be opened"), ): # pylint: disable-next=consider-using-with @@ -209,7 +209,7 @@ async def _test_downloading_encrypted_backup( dec_metadata = json.loads(outer_tar.extractfile("./backup.json").read()) assert dec_metadata == enc_metadata | {"protected": False} with ( - outer_tar.extractfile("core.tar.gz") as inner_tar_file, + outer_tar.extractfile("homeassistant.tar.gz") as inner_tar_file, tarfile.open(fileobj=inner_tar_file, mode="r") as inner_tar, ): assert inner_tar.getnames() == [ diff --git a/tests/components/backup/test_util.py b/tests/components/backup/test_util.py index 97e94eafb73..a999672e7f6 100644 --- a/tests/components/backup/test_util.py +++ b/tests/components/backup/test_util.py @@ -174,7 +174,10 @@ async def test_decrypted_backup_streamer(hass: HomeAssistant) -> None: ) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -218,7 +221,10 @@ async def test_decrypted_backup_streamer_interrupt_stuck_reader( """Test the decrypted backup streamer.""" encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -253,7 +259,10 @@ async def test_decrypted_backup_streamer_interrupt_stuck_writer( """Test the decrypted backup streamer.""" encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -283,7 +292,10 @@ async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) -> """Test the decrypted backup streamer with wrong password.""" encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -320,7 +332,10 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: ) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -353,15 +368,16 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: bytes.fromhex("00000000000000000000000000000000"), ) encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2") - assert encryptor.backup() == dataclasses.replace( - backup, protected=True, size=backup.size + len(expected_padding) - ) - encrypted_stream = await encryptor.open_stream() - encrypted_output = b"" - async for chunk in encrypted_stream: - encrypted_output += chunk - await encryptor.wait() + assert encryptor.backup() == dataclasses.replace( + backup, protected=True, size=backup.size + len(expected_padding) + ) + + encrypted_stream = await encryptor.open_stream() + encrypted_output = b"" + async for chunk in encrypted_stream: + encrypted_output += chunk + await encryptor.wait() # Expect the output to match the stored encrypted backup file, with additional # padding. @@ -377,7 +393,10 @@ async def test_encrypted_backup_streamer_interrupt_stuck_reader( "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -414,7 +433,10 @@ async def test_encrypted_backup_streamer_interrupt_stuck_writer( "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -447,7 +469,10 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No ) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -490,7 +515,7 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No await encryptor1.wait() await encryptor2.wait() - # Output from the two streames should differ but have the same length. + # Output from the two streams should differ but have the same length. assert encrypted_output1 != encrypted_output3 assert len(encrypted_output1) == len(encrypted_output3) @@ -508,7 +533,10 @@ async def test_encrypted_backup_streamer_error(hass: HomeAssistant) -> None: "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, From fd6fb7e3bcc56b37db39a0c32ed0e35fe50c0044 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 14:55:03 -0500 Subject: [PATCH 009/109] Bump forecast-solar to 4.2.0 (#144502) --- homeassistant/components/forecast_solar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json index 769bda56adc..66796a44dc4 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/forecast_solar", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["forecast-solar==4.1.0"] + "requirements": ["forecast-solar==4.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ff231fb44ef..6f4dce5f130 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ fnv-hash-fast==1.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==4.1.0 +forecast-solar==4.2.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0d5fffe420..cfd59c5ee2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -818,7 +818,7 @@ fnv-hash-fast==1.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==4.1.0 +forecast-solar==4.2.0 # homeassistant.components.freebox freebox-api==1.2.2 From 47acceea082cac8fd74b93ed436755899d1f0e94 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 8 May 2025 21:54:49 +0200 Subject: [PATCH 010/109] Fix removing of smarthome templates on startup of AVM Fritz!SmartHome integration (#144506) --- .../components/fritzbox/coordinator.py | 2 +- tests/components/fritzbox/test_coordinator.py | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index adc63dd2c2e..8a37ebf63e4 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -92,7 +92,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat available_main_ains = [ ain - for ain, dev in data.devices.items() + for ain, dev in data.devices.items() | data.templates.items() if dev.device_and_unit_id[1] is None ] device_reg = dr.async_get(self.hass) diff --git a/tests/components/fritzbox/test_coordinator.py b/tests/components/fritzbox/test_coordinator.py index f4f4da90181..4c329daa640 100644 --- a/tests/components/fritzbox/test_coordinator.py +++ b/tests/components/fritzbox/test_coordinator.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from . import FritzDeviceCoverMock, FritzDeviceSwitchMock +from . import FritzDeviceCoverMock, FritzDeviceSwitchMock, FritzEntityBaseMock from .const import MOCK_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed @@ -84,6 +84,8 @@ async def test_coordinator_automatic_registry_cleanup( entity_registry: er.EntityRegistry, ) -> None: """Test automatic registry cleanup.""" + + # init with 2 devices and 1 template fritz().get_devices.return_value = [ FritzDeviceSwitchMock( ain="fake ain switch", @@ -96,6 +98,13 @@ async def test_coordinator_automatic_registry_cleanup( name="fake_cover", ), ] + fritz().get_templates.return_value = [ + FritzEntityBaseMock( + ain="fake ain template", + device_and_unit_id=("fake ain template", None), + name="fake_template", + ) + ] entry = MockConfigEntry( domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], @@ -105,9 +114,10 @@ async def test_coordinator_automatic_registry_cleanup( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) - assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 19 - assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 20 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 3 + # remove one device, keep the template fritz().get_devices.return_value = [ FritzDeviceSwitchMock( ain="fake ain switch", @@ -119,5 +129,14 @@ async def test_coordinator_automatic_registry_cleanup( async_fire_time_changed(hass, utcnow() + timedelta(seconds=35)) await hass.async_block_till_done(wait_background_tasks=True) + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 13 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 + + # remove the template, keep the device + fritz().get_templates.return_value = [] + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=35)) + await hass.async_block_till_done(wait_background_tasks=True) + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 12 assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1 From cb475bf153488717ef58ba08546aae30a63c8189 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 15:43:45 -0500 Subject: [PATCH 011/109] Bump aiodns to 3.4.0 (#144511) --- homeassistant/components/dnsip/config_flow.py | 2 +- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/components/dnsip/sensor.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index 9e98178e718..e7b60d5bd6f 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -68,7 +68,7 @@ async def async_validate_hostname( result = False with contextlib.suppress(DNSError): result = bool( - await aiodns.DNSResolver( + await aiodns.DNSResolver( # type: ignore[call-overload] nameservers=[resolver], udp_port=port, tcp_port=port ).query(hostname, qtype) ) diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 35802adb7f3..e004b386e02 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dnsip", "iot_class": "cloud_polling", - "requirements": ["aiodns==3.3.0"] + "requirements": ["aiodns==3.4.0"] } diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 6708baefe8c..6cdb67dd80d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -106,7 +106,7 @@ class WanIpSensor(SensorEntity): async def async_update(self) -> None: """Get the current DNS IP address for hostname.""" try: - response = await self.resolver.query(self.hostname, self.querytype) + response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload] except DNSError as err: _LOGGER.warning("Exception while resolving host: %s", err) response = None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 48b21942f4d..d26a2abc59b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 -aiodns==3.3.0 +aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 50cc169cf10..2299673ea61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.13.2" dependencies = [ - "aiodns==3.3.0", + "aiodns==3.4.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 diff --git a/requirements.txt b/requirements.txt index 26ff191025f..e87c1750336 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiodns==3.3.0 +aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp==3.11.18 aiohttp_cors==0.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6f4dce5f130..c75d8ed43ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.3.0 +aiodns==3.4.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfd59c5ee2d..70c2427a11c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.3.0 +aiodns==3.4.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 From 4ad387c967ba2069103cd43abc3e83a1cc11b838 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 9 May 2025 10:37:48 +0200 Subject: [PATCH 012/109] Fix statistics coordinator subscription for lamarzocco (#144541) --- homeassistant/components/lamarzocco/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 5dc0eb3dbef..6e3eee50f41 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -132,17 +132,18 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up sensor entities.""" - coordinator = entry.runtime_data.config_coordinator + config_coordinator = entry.runtime_data.config_coordinator + statistic_coordinators = entry.runtime_data.statistics_coordinator entities = [ - LaMarzoccoSensorEntity(coordinator, description) + LaMarzoccoSensorEntity(config_coordinator, description) for description in ENTITIES - if description.supported_fn(coordinator) + if description.supported_fn(config_coordinator) ] entities.extend( - LaMarzoccoStatisticSensorEntity(coordinator, description) + LaMarzoccoStatisticSensorEntity(statistic_coordinators, description) for description in STATISTIC_ENTITIES - if description.supported_fn(coordinator) + if description.supported_fn(statistic_coordinators) ) async_add_entities(entities) From 196d923ac6009ef4f23e9cb474555e93bd7aa2ab Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 9 May 2025 13:09:05 +0200 Subject: [PATCH 013/109] Update frontend to 20250509.0 (#144549) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 84062384bf5..9471f863a72 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250507.0"] + "requirements": ["home-assistant-frontend==20250509.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d26a2abc59b..bd3ec0bb03f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250507.0 +home-assistant-frontend==20250509.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c75d8ed43ec..7973c10cbe3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250507.0 +home-assistant-frontend==20250509.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70c2427a11c..0e41fc37f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250507.0 +home-assistant-frontend==20250509.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From 181eca6c8200c20d99eb1ff977950b24cda65383 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 May 2025 14:47:38 +0200 Subject: [PATCH 014/109] Reolink clean device registry mac (#144554) --- homeassistant/components/reolink/__init__.py | 10 +++- tests/components/reolink/test_init.py | 52 +++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index f7d13c1d90f..433af396d63 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -380,6 +380,14 @@ def migrate_entity_ids( 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) + 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"): diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 5915bd06608..6b57c1c253f 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -39,7 +39,7 @@ from homeassistant.helpers import ( entity_registry as er, issue_registry as ir, ) -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.setup import async_setup_component from .conftest import ( @@ -51,6 +51,7 @@ from .conftest import ( TEST_HOST, TEST_HOST_MODEL, TEST_MAC, + TEST_MAC_CAM, TEST_NVR_NAME, TEST_PORT, TEST_PRIVACY, @@ -614,6 +615,55 @@ async def test_migrate_with_already_existing_entity( assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) +async def test_cleanup_mac_connection( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup of the MAC of a IPC which was set to the MAC of the host.""" + reolink_connect.channels = [0] + reolink_connect.baichuan.mac_address.return_value = None + entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + + dev_entry = device_registry.async_get_or_create( + identifiers={(DOMAIN, dev_id)}, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, + 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.connections == {(CONNECTION_NETWORK_MAC, TEST_MAC)} + + # 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.connections == set() + + reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From f392e0c1c712d1a1dd55331c691b0c062bea5e9a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 May 2025 14:50:00 +0200 Subject: [PATCH 015/109] Prevent errors during cleaning of connections/identifiers in device registry (#144558) --- homeassistant/helpers/device_registry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 79d6774c407..a80e74e7eb2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -575,9 +575,11 @@ class DeviceRegistryItems[_EntryTypeT: (DeviceEntry, DeletedDeviceEntry)]( """Unindex an entry.""" old_entry = self.data[key] for connection in old_entry.connections: - del self._connections[connection] + if connection in self._connections: + del self._connections[connection] for identifier in old_entry.identifiers: - del self._identifiers[identifier] + if identifier in self._identifiers: + del self._identifiers[identifier] def get_entry( self, From 13aba6201e5027478f95bcda54d4d045572ec5a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 May 2025 13:29:29 +0000 Subject: [PATCH 016/109] Bump version to 2025.5.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 11abbd33b41..65f8e2bae64 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 2299673ea61..fa960f6f815 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0" +version = "2025.5.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 010b04437930d8a53715daf7888fced574c1d5fe Mon Sep 17 00:00:00 2001 From: TimL Date: Sat, 10 May 2025 02:31:00 +1000 Subject: [PATCH 017/109] Allow dns hostnames to be retained for SMLIGHT user flow. (#142514) * Dont overwrite host with local IP * adjust test for user flow change --- homeassistant/components/smlight/config_flow.py | 2 -- tests/components/smlight/conftest.py | 1 + tests/components/smlight/test_config_flow.py | 16 +++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smlight/config_flow.py b/homeassistant/components/smlight/config_flow.py index ce4f8f43233..39750bdc422 100644 --- a/homeassistant/components/smlight/config_flow.py +++ b/homeassistant/components/smlight/config_flow.py @@ -53,7 +53,6 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN): try: if not await self._async_check_auth_required(user_input): info = await self.client.get_info() - self._host = str(info.device_ip) self._device_name = str(info.hostname) if info.model not in Devices: @@ -79,7 +78,6 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN): try: if not await self._async_check_auth_required(user_input): info = await self.client.get_info() - self._host = str(info.device_ip) self._device_name = str(info.hostname) if info.model not in Devices: diff --git a/tests/components/smlight/conftest.py b/tests/components/smlight/conftest.py index 7a1b16f1d6b..6c056c95fd9 100644 --- a/tests/components/smlight/conftest.py +++ b/tests/components/smlight/conftest.py @@ -21,6 +21,7 @@ from tests.common import ( MOCK_DEVICE_NAME = "slzb-06" MOCK_HOST = "192.168.1.161" +MOCK_HOSTNAME = "slzb-06p7.lan" MOCK_USERNAME = "test-user" MOCK_PASSWORD = "test-pass" diff --git a/tests/components/smlight/test_config_flow.py b/tests/components/smlight/test_config_flow.py index 4ecfe9366e3..497cb8d9484 100644 --- a/tests/components/smlight/test_config_flow.py +++ b/tests/components/smlight/test_config_flow.py @@ -15,7 +15,13 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .conftest import MOCK_DEVICE_NAME, MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME +from .conftest import ( + MOCK_DEVICE_NAME, + MOCK_HOST, + MOCK_HOSTNAME, + MOCK_PASSWORD, + MOCK_USERNAME, +) from tests.common import MockConfigEntry @@ -53,14 +59,14 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: "slzb-06p7.local", + CONF_HOST: MOCK_HOSTNAME, }, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SLZB-06p7" assert result2["data"] == { - CONF_HOST: MOCK_HOST, + CONF_HOST: MOCK_HOSTNAME, } assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 @@ -82,7 +88,7 @@ async def test_user_flow_auth( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: "slzb-06p7.local", + CONF_HOST: MOCK_HOSTNAME, }, ) assert result2["type"] is FlowResultType.FORM @@ -100,7 +106,7 @@ async def test_user_flow_auth( assert result3["data"] == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, - CONF_HOST: MOCK_HOST, + CONF_HOST: MOCK_HOSTNAME, } assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 From 7d163aa659cf0e00738eab5f4d00c2b7f63fd9aa Mon Sep 17 00:00:00 2001 From: Seweryn Zeman Date: Sun, 11 May 2025 20:33:17 +0200 Subject: [PATCH 018/109] Removed unused file_id param from open_ai_conversation request (#143878) --- homeassistant/components/openai_conversation/__init__.py | 1 - tests/components/openai_conversation/test_init.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 7da1becd333..71effe83884 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -140,7 +140,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: content.append( ResponseInputImageParam( type="input_image", - file_id=filename, image_url=f"data:{mime_type};base64,{base64_file}", detail="auto", ) diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index dc83aa48807..b4f816707e9 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -324,7 +324,6 @@ async def test_init_error( "type": "input_image", "image_url": "data:image/jpeg;base64,BASE64IMAGE1", "detail": "auto", - "file_id": "/a/b/c.jpg", }, ], }, @@ -349,13 +348,11 @@ async def test_init_error( "type": "input_image", "image_url": "data:image/jpeg;base64,BASE64IMAGE1", "detail": "auto", - "file_id": "/a/b/c.jpg", }, { "type": "input_image", "image_url": "data:image/jpeg;base64,BASE64IMAGE2", "detail": "auto", - "file_id": "d/e/f.jpg", }, ], }, From bde04bc47b21b1c02d7012fbae3106e225575cd9 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Wed, 14 May 2025 11:44:59 +0200 Subject: [PATCH 019/109] Doorbell Event is fired just once in homematicip_cloud (#144357) * fire event if event type if correct * Fix requested changes --- .../components/homematicip_cloud/event.py | 37 +++++++++++++++++-- .../homematicip_cloud/test_event.py | 29 +++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/event.py b/homeassistant/components/homematicip_cloud/event.py index 47a5ff46224..fc7f43bad1a 100644 --- a/homeassistant/components/homematicip_cloud/event.py +++ b/homeassistant/components/homematicip_cloud/event.py @@ -1,8 +1,11 @@ """Support for HomematicIP Cloud events.""" +from collections.abc import Callable from dataclasses import dataclass from typing import TYPE_CHECKING +from homematicip.base.channel_event import ChannelEvent +from homematicip.base.functionalChannels import FunctionalChannel from homematicip.device import Device from homeassistant.components.event import ( @@ -23,6 +26,9 @@ from .hap import HomematicipHAP class HmipEventEntityDescription(EventEntityDescription): """Description of a HomematicIP Cloud event.""" + channel_event_types: list[str] | None = None + channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None + EVENT_DESCRIPTIONS = { "doorbell": HmipEventEntityDescription( @@ -30,6 +36,8 @@ EVENT_DESCRIPTIONS = { translation_key="doorbell", device_class=EventDeviceClass.DOORBELL, event_types=["ring"], + channel_event_types=["DOOR_BELL_SENSOR_EVENT"], + channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT", ), } @@ -41,24 +49,29 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[DOMAIN][config_entry.unique_id] + entities: list[HomematicipGenericEntity] = [] - async_add_entities( + entities.extend( HomematicipDoorBellEvent( hap, device, channel.index, - EVENT_DESCRIPTIONS["doorbell"], + description, ) + for description in EVENT_DESCRIPTIONS.values() for device in hap.home.devices for channel in device.functionalChannels - if channel.channelRole == "DOOR_BELL_INPUT" + if description.channel_selector_fn and description.channel_selector_fn(channel) ) + async_add_entities(entities) + class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity): """Event class for HomematicIP doorbell events.""" _attr_device_class = EventDeviceClass.DOORBELL + entity_description: HmipEventEntityDescription def __init__( self, @@ -86,9 +99,27 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity): @callback def _async_handle_event(self, *args, **kwargs) -> None: """Handle the event fired by the functional channel.""" + raised_channel_event = self._get_channel_event_from_args(*args) + + if not self._should_raise(raised_channel_event): + return + event_types = self.entity_description.event_types if TYPE_CHECKING: assert event_types is not None self._trigger_event(event_type=event_types[0]) self.async_write_ha_state() + + def _should_raise(self, event_type: str) -> bool: + """Check if the event should be raised.""" + if self.entity_description.channel_event_types is None: + return False + return event_type in self.entity_description.channel_event_types + + def _get_channel_event_from_args(self, *args) -> str: + """Get the channel event.""" + if isinstance(args[0], ChannelEvent): + return args[0].channelEventType + + return "" diff --git a/tests/components/homematicip_cloud/test_event.py b/tests/components/homematicip_cloud/test_event.py index de615b35808..fcd16ca62d5 100644 --- a/tests/components/homematicip_cloud/test_event.py +++ b/tests/components/homematicip_cloud/test_event.py @@ -35,3 +35,32 @@ async def test_door_bell_event( ha_state = hass.states.get(entity_id) assert ha_state.state != STATE_UNKNOWN + + +async def test_door_bell_event_wrong_event_type( + hass: HomeAssistant, + default_mock_hap_factory: HomeFactory, +) -> None: + """Test of door bell event of HmIP-DSD-PCB.""" + entity_id = "event.dsdpcb_klingel_doorbell" + entity_name = "dsdpcb_klingel doorbell" + device_model = "HmIP-DSD-PCB" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["dsdpcb_klingel"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + ch = hmip_device.functionalChannels[1] + channel_event = ChannelEvent( + channelEventType="KEY_PRESS", channelIndex=1, deviceId=ch.device.id + ) + + assert ha_state.state == STATE_UNKNOWN + + ch.fire_channel_event(channel_event) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN From 48aa6be8895c8bbbacc0e111d9624f2a9ce37137 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Fri, 9 May 2025 12:50:55 -0400 Subject: [PATCH 020/109] Don't scale Roborock mop Path (#144421) don't scale mop path --- homeassistant/components/roborock/coordinator.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 2439a4f904a..dc0677b25d2 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -28,7 +28,7 @@ from roborock.version_a01_apis import RoborockClientA01 from roborock.web_api import RoborockApiClient from vacuum_map_parser_base.config.color import ColorsPalette from vacuum_map_parser_base.config.image_config import ImageConfig -from vacuum_map_parser_base.config.size import Sizes +from vacuum_map_parser_base.config.size import Size, Sizes from vacuum_map_parser_base.map_data import MapData from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser @@ -148,7 +148,13 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ] self.map_parser = RoborockMapDataParser( ColorsPalette(), - Sizes({k: v * MAP_SCALE for k, v in Sizes.SIZES.items()}), + Sizes( + { + k: v * MAP_SCALE + for k, v in Sizes.SIZES.items() + if k != Size.MOP_PATH_WIDTH + } + ), drawables, ImageConfig(scale=MAP_SCALE), [], From 2fbc75f89bd5dd565409ca67b204ba50d21f5b12 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 May 2025 18:46:32 +0200 Subject: [PATCH 021/109] Reolink fix privacy mode availability for NVR IPC cams (#144569) * Correct "available" for IPC cams * Check privacy mode when updating --- homeassistant/components/reolink/entity.py | 9 ++++++++- homeassistant/components/reolink/host.py | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index ec598de663d..3d66939a13c 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -198,7 +198,14 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._host.api.camera_online(self._channel) + if self.entity_description.always_available: + return True + + return ( + super().available + and self._host.api.camera_online(self._channel) + and not self._host.api.baichuan.privacy_mode(self._channel) + ) def register_callback(self, callback_id: str, cmd_id: int) -> None: """Register callback for TCP push events.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a027177f1fc..378c167d469 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -465,10 +465,11 @@ class ReolinkHost: wake = True self.last_wake = time() + for channel in self._api.channels: + if self._api.baichuan.privacy_mode(channel): + await self._api.baichuan.get_privacy_mode(channel) if self._api.baichuan.privacy_mode(): - await self._api.baichuan.get_privacy_mode() - if self._api.baichuan.privacy_mode(): - return # API is shutdown, no need to check states + return # API is shutdown, no need to check states await self._api.get_states(cmd_list=self.update_cmd, wake=wake) From 36a35132c0371c75da8af0220ea778610a7849b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 May 2025 11:59:38 -0500 Subject: [PATCH 022/109] Bump aiodiscover to 2.7.0 (#144571) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 64fd2ff38c6..c425aafdb00 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.1.1", - "aiodiscover==2.6.1", + "aiodiscover==2.7.0", "cached-ipaddress==0.10.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bd3ec0bb03f..d457184d4f5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodhcpwatcher==1.1.1 -aiodiscover==2.6.1 +aiodiscover==2.7.0 aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 7973c10cbe3..18466a4bdd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aiocomelit==0.12.0 aiodhcpwatcher==1.1.1 # homeassistant.components.dhcp -aiodiscover==2.6.1 +aiodiscover==2.7.0 # homeassistant.components.dnsip aiodns==3.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e41fc37f4a..409d91af9b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ aiocomelit==0.12.0 aiodhcpwatcher==1.1.1 # homeassistant.components.dhcp -aiodiscover==2.6.1 +aiodiscover==2.7.0 # homeassistant.components.dnsip aiodns==3.4.0 From 5a95f43992addd17b01d354c9f935112c4eb6a03 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 12 May 2025 00:15:30 +0200 Subject: [PATCH 023/109] Bump reolink_aio to 0.13.3 (#144583) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 59a2741571f..a6f0b59426a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.2"] + "requirements": ["reolink-aio==0.13.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 18466a4bdd1..f88325f892f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2637,7 +2637,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.2 +reolink-aio==0.13.3 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 409d91af9b6..f3c4c3ec31a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2144,7 +2144,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.2 +reolink-aio==0.13.3 # homeassistant.components.rflink rflink==0.0.66 From e9cc624d93121155d98fa002605b7822028235f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 May 2025 17:19:00 -0500 Subject: [PATCH 024/109] Mark inkbird coordinator as not needing connectable (#144584) --- homeassistant/components/inkbird/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/inkbird/coordinator.py b/homeassistant/components/inkbird/coordinator.py index d52ebd83595..fbacedf7e0f 100644 --- a/homeassistant/components/inkbird/coordinator.py +++ b/homeassistant/components/inkbird/coordinator.py @@ -58,6 +58,7 @@ class INKBIRDActiveBluetoothProcessorCoordinator( update_method=self._async_on_update, needs_poll_method=self._async_needs_poll, poll_method=self._async_poll_data, + connectable=False, # Polling only happens if active scanning is disabled ) async def async_init(self) -> None: From 27db4e90b52015c51219059c8e0f2fe9481e1441 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 10 May 2025 20:23:52 +0200 Subject: [PATCH 025/109] fix enphase_envoy diagnostics home endpoint name (#144634) --- homeassistant/components/enphase_envoy/diagnostics.py | 2 +- .../enphase_envoy/snapshots/test_diagnostics.ambr | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 6fcf73bebe9..97079255876 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -64,7 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]: "/ivp/ensemble/generator", "/ivp/meters", "/ivp/meters/readings", - "/home,", + "/home", ] for end_point in end_points: diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index acbd7de6c0e..650fb0bb810 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -896,8 +896,8 @@ '/api/v1/production/inverters': 'Testing request replies.', '/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}', '/api/v1/production_log': '{"headers":{"Hello":"World"},"code":200}', - '/home,': 'Testing request replies.', - '/home,_log': '{"headers":{"Hello":"World"},"code":200}', + '/home': 'Testing request replies.', + '/home_log': '{"headers":{"Hello":"World"},"code":200}', '/info': 'Testing request replies.', '/info_log': '{"headers":{"Hello":"World"},"code":200}', '/ivp/ensemble/dry_contacts': 'Testing request replies.', @@ -1390,7 +1390,7 @@ '/api/v1/production_log': dict({ 'Error': "EnvoyError('Test')", }), - '/home,_log': dict({ + '/home_log': dict({ 'Error': "EnvoyError('Test')", }), '/info_log': dict({ From a54816a6e51cd323dffde68d86a5e76f02a87691 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sun, 11 May 2025 17:07:33 +0200 Subject: [PATCH 026/109] Bump pylamarzocco to 2.0.2 (#144635) Co-authored-by: Shay Levy --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index fb6a3660c66..1fbef073394 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.1"] + "requirements": ["pylamarzocco==2.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f88325f892f..4e91ce1aca8 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.1 +pylamarzocco==2.0.2 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3c4c3ec31a..14b2e100b6b 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.1 +pylamarzocco==2.0.2 # homeassistant.components.lastfm pylast==5.1.0 From ca143222276e9795bb76312a72ccaa05a3eb54d7 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sun, 11 May 2025 13:07:35 +0200 Subject: [PATCH 027/109] bump pyenphase to 1.26.1 (#144641) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 4516a35f4fe..e978ded7321 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==1.26.0"], + "requirements": ["pyenphase==1.26.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 4e91ce1aca8..f1af6720606 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1955,7 +1955,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.26.0 +pyenphase==1.26.1 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14b2e100b6b..b8ceb03eecc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1600,7 +1600,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.26.0 +pyenphase==1.26.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 081afe6034646968968c7b09e499135f0934620c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 10 May 2025 20:59:01 -0700 Subject: [PATCH 028/109] Bump ical to 9.2.1 (#144642) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 32af3e675b3..668ab6e34be 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index eba26e88d5a..c3ffce2890b 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.0"] + "requirements": ["ical==9.2.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index fb48ca72337..f93129be94c 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==9.2.0"] + "requirements": ["ical==9.2.1"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index b31fa3389dc..4df3f11cf10 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.0"] + "requirements": ["ical==9.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index f1af6720606..1fcb506ed9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1200,7 +1200,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.0 +ical==9.2.1 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8ceb03eecc..2dd70218d32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.0 +ical==9.2.1 # homeassistant.components.caldav icalendar==6.1.0 From 0635856761ecda49e217cdedeb3d094eb7f9a516 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 11 May 2025 12:56:40 +0200 Subject: [PATCH 029/109] Bump python-linkplay to v0.2.5 (#144666) Bump linkplay to 0.2.5 --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 69a7b71eeb6..ac89d2ff399 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.4"], + "requirements": ["python-linkplay==0.2.5"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 1fcb506ed9a..21fb24d7001 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2437,7 +2437,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.4 +python-linkplay==0.2.5 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dd70218d32..2530baafd43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1980,7 +1980,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.4 +python-linkplay==0.2.5 # homeassistant.components.matter python-matter-server==7.0.0 From 543348fe581645eee1ca5c5d3205c97e123fa974 Mon Sep 17 00:00:00 2001 From: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Date: Sun, 11 May 2025 21:06:04 +0200 Subject: [PATCH 030/109] Close Octoprint aiohttp session on unload (#144670) --- homeassistant/components/octoprint/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 59fd04357eb..48d81b81f0c 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -181,11 +181,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = aiohttp.ClientSession(connector=connector) @callback - def _async_close_websession(event: Event) -> None: + def _async_close_websession(event: Event | None = None) -> None: """Close websession.""" session.detach() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close_websession) + entry.async_on_unload(_async_close_websession) + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_close_websession) + ) client = OctoprintClient( host=entry.data[CONF_HOST], From 358b0c1c17614922a255fe6c4bc4e67da5d3dea6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 May 2025 17:00:44 +0200 Subject: [PATCH 031/109] Bump holidays to 0.72 (#144671) --- 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 d54d6955087..9809862cd52 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.70", "babel==2.15.0"] + "requirements": ["holidays==0.72", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 60196fb15b7..542b68169a3 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.70"] + "requirements": ["holidays==0.72"] } diff --git a/requirements_all.txt b/requirements_all.txt index 21fb24d7001..85af7abd356 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.70 +holidays==0.72 # homeassistant.components.frontend home-assistant-frontend==20250509.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2530baafd43..d934cc39e86 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.70 +holidays==0.72 # homeassistant.components.frontend home-assistant-frontend==20250509.0 From da79d5b2e3c3f05264aeb6ff5e85e9e8604ad7ff Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 11 May 2025 18:01:51 +0300 Subject: [PATCH 032/109] Fix strings typo for Comelit (#144672) --- homeassistant/components/comelit/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 2076ecb5c1e..8f2ae1433e5 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -76,7 +76,7 @@ "cannot_authenticate": { "message": "Error authenticating" }, - "updated_failed": { + "update_failed": { "message": "Failed to update data: {error}" } } From cf0911cc5697fbc68b8cbc61cd6f469e9813a003 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 11 May 2025 22:00:21 +0300 Subject: [PATCH 033/109] Avoid closing shared session for Comelit (#144682) --- homeassistant/components/comelit/__init__.py | 1 - homeassistant/components/comelit/config_flow.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py index c2a7498afec..23be67fc1a1 100644 --- a/homeassistant/components/comelit/__init__.py +++ b/homeassistant/components/comelit/__init__.py @@ -77,6 +77,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> coordinator = entry.runtime_data if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): await coordinator.api.logout() - await coordinator.api.close() return unload_ok diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index f6bda97a781..10180236f79 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -73,7 +73,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, ) from err finally: await api.logout() - await api.close() return {"title": data[CONF_HOST]} From 47b45444eb8f39f5a09dbe15ad8fe96799a2ff4f Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 12 May 2025 09:39:30 +0200 Subject: [PATCH 034/109] Fix wrong state in Husqvarna Automower (#144684) --- .../components/husqvarna_automower/lawn_mower.py | 8 ++++---- tests/components/husqvarna_automower/test_lawn_mower.py | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index 9ae214524a7..5a728265651 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -110,14 +110,14 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): mower_attributes = self.mower_attributes if mower_attributes.mower.state in PAUSED_STATES: return LawnMowerActivity.PAUSED - if mower_attributes.mower.state in MowerStates.IN_OPERATION: - if mower_attributes.mower.activity == MowerActivities.GOING_HOME: - return LawnMowerActivity.RETURNING - return LawnMowerActivity.MOWING if (mower_attributes.mower.state == "RESTRICTED") or ( mower_attributes.mower.activity in DOCKED_ACTIVITIES ): return LawnMowerActivity.DOCKED + if mower_attributes.mower.state in MowerStates.IN_OPERATION: + if mower_attributes.mower.activity == MowerActivities.GOING_HOME: + return LawnMowerActivity.RETURNING + return LawnMowerActivity.MOWING return LawnMowerActivity.ERROR @property diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 12c53d709ca..de7479bf908 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -37,6 +37,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed MowerStates.IN_OPERATION, LawnMowerActivity.MOWING, ), + ( + MowerActivities.PARKED_IN_CS, + MowerStates.IN_OPERATION, + LawnMowerActivity.DOCKED, + ), ], ) async def test_lawn_mower_states( From e465276464402da34c9b9067a7a27950ebd4a374 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 11 May 2025 18:01:20 -0700 Subject: [PATCH 035/109] Bump voluptuous-openapi to 0.1.0 (#144703) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d457184d4f5..a42910fc7e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -70,7 +70,7 @@ typing-extensions>=4.13.0,<5.0 ulid-transform==1.4.0 urllib3>=1.26.5,<2 uv==0.7.1 -voluptuous-openapi==0.0.7 +voluptuous-openapi==0.1.0 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 diff --git a/pyproject.toml b/pyproject.toml index fa960f6f815..ad3f9ad86e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ dependencies = [ "uv==0.7.1", "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", - "voluptuous-openapi==0.0.7", + "voluptuous-openapi==0.1.0", "yarl==1.20.0", "webrtc-models==0.3.0", "zeroconf==0.147.0", diff --git a/requirements.txt b/requirements.txt index e87c1750336..27095417cb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,7 +57,7 @@ urllib3>=1.26.5,<2 uv==0.7.1 voluptuous==0.15.2 voluptuous-serialize==2.6.0 -voluptuous-openapi==0.0.7 +voluptuous-openapi==0.1.0 yarl==1.20.0 webrtc-models==0.3.0 zeroconf==0.147.0 From f1a3d62db2c772132fc05c72163bbd484a32b804 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 12 May 2025 00:37:57 -0700 Subject: [PATCH 036/109] Bump ical to 9.2.2 (#144713) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 668ab6e34be..296ac519e1d 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.1"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.2"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index c3ffce2890b..2fa603d51ff 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.1"] + "requirements": ["ical==9.2.2"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index f93129be94c..735c11e645a 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==9.2.1"] + "requirements": ["ical==9.2.2"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 4df3f11cf10..33a46ea3dc8 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.1"] + "requirements": ["ical==9.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 85af7abd356..33b97d2ea4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1200,7 +1200,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.1 +ical==9.2.2 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d934cc39e86..12be241c790 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.1 +ical==9.2.2 # homeassistant.components.caldav icalendar==6.1.0 From 41a503f76fdf69f5b465c13af457d0a6b9751111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Mon, 12 May 2025 19:45:34 +0200 Subject: [PATCH 037/109] Bump gcal-sync to 7.0.1 (#144718) Co-authored-by: Allen Porter --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 296ac519e1d..b43ded01d6e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.2"] + "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 33b97d2ea4d..a6188612341 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ gardena-bluetooth==1.6.0 gassist-text==0.0.12 # homeassistant.components.google -gcal-sync==7.0.0 +gcal-sync==7.0.1 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12be241c790..4aedcccd174 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -840,7 +840,7 @@ gardena-bluetooth==1.6.0 gassist-text==0.0.12 # homeassistant.components.google -gcal-sync==7.0.0 +gcal-sync==7.0.1 # homeassistant.components.geniushub geniushub-client==0.7.1 From a4a7601f9ff2e40556be58eb2d99b775e56b49a7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 12 May 2025 12:23:44 +0300 Subject: [PATCH 038/109] Bump aiocomelit to 0.12.1 (#144720) --- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 2097d1c25f6..58f347b4ba3 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.0"] + "requirements": ["aiocomelit==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a6188612341..67230b4bfcb 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.0 +aiocomelit==0.12.1 # homeassistant.components.dhcp aiodhcpwatcher==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4aedcccd174..3de21584bfe 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.0 +aiocomelit==0.12.1 # homeassistant.components.dhcp aiodhcpwatcher==1.1.1 From f25e50b017e3f0fecb91c819d34555102729970e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 15 May 2025 10:56:54 +0200 Subject: [PATCH 039/109] Fix Netgear handeling of missing MAC in device registry (#144722) --- homeassistant/components/netgear/router.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index d81f556193b..23ee47e7a2d 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -150,7 +150,11 @@ class NetgearRouter: if device_entry.via_device_id is None: continue # do not add the router itself - device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC] + device_mac = dict(device_entry.connections).get( + dr.CONNECTION_NETWORK_MAC + ) + if device_mac is None: + continue self.devices[device_mac] = { "mac": device_mac, "name": device_entry.name, From b69ebdaecb853a52ca696a18d022a3e9bab4d1cd Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 12 May 2025 21:18:55 +0200 Subject: [PATCH 040/109] Repair Z-Wave unknown controller (#144738) Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/__init__.py | 33 +++++ homeassistant/components/zwave_js/repairs.py | 44 +++++++ .../components/zwave_js/strings.json | 11 ++ tests/components/zwave_js/conftest.py | 6 +- tests/components/zwave_js/test_repairs.py | 116 ++++++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index e73bd01deba..349baecc21d 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -278,6 +278,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # and we'll handle the clean up below. await driver_events.setup(driver) + if (old_unique_id := entry.unique_id) is not None and old_unique_id != ( + new_unique_id := str(driver.controller.home_id) + ): + device_registry = dr.async_get(hass) + controller_model = "Unknown model" + if ( + (own_node := driver.controller.own_node) + and ( + controller_device_entry := device_registry.async_get_device( + identifiers={get_device_id(driver, own_node)} + ) + ) + and (model := controller_device_entry.model) + ): + controller_model = model + async_create_issue( + hass, + DOMAIN, + f"migrate_unique_id.{entry.entry_id}", + data={ + "config_entry_id": entry.entry_id, + "config_entry_title": entry.title, + "controller_model": controller_model, + "new_unique_id": new_unique_id, + "old_unique_id": old_unique_id, + }, + is_fixable=True, + severity=IssueSeverity.ERROR, + translation_key="migrate_unique_id", + ) + else: + async_delete_issue(hass, DOMAIN, f"migrate_unique_id.{entry.entry_id}") + # If the listen task is already failed, we need to raise ConfigEntryNotReady if listen_task.done(): listen_error, error_message = _get_listen_task_error(listen_task) diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index e515ae10549..f1deb91d869 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -57,6 +57,47 @@ class DeviceConfigFileChangedFlow(RepairsFlow): ) +class MigrateUniqueIDFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, data: dict[str, str]) -> None: + """Initialize.""" + self.description_placeholders: dict[str, str] = { + "config_entry_title": data["config_entry_title"], + "controller_model": data["controller_model"], + "new_unique_id": data["new_unique_id"], + "old_unique_id": data["old_unique_id"], + } + self._config_entry_id: str = data["config_entry_id"] + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + config_entry = self.hass.config_entries.async_get_entry( + self._config_entry_id + ) + # If config entry was removed, we can ignore the issue. + if config_entry is not None: + self.hass.config_entries.async_update_entry( + config_entry, + unique_id=self.description_placeholders["new_unique_id"], + ) + return self.async_create_entry(data={}) + + return self.async_show_form( + step_id="confirm", + description_placeholders=self.description_placeholders, + ) + + async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, data: dict[str, str] | None ) -> RepairsFlow: @@ -65,4 +106,7 @@ async def async_create_fix_flow( if issue_id.split(".")[0] == "device_config_file_changed": assert data return DeviceConfigFileChangedFlow(data) + if issue_id.split(".")[0] == "migrate_unique_id": + assert data + return MigrateUniqueIDFlow(data) return ConfirmRepairFlow() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 56ae4e12401..2a8e2c6ea2d 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -273,6 +273,17 @@ "invalid_server_version": { "description": "The version of Z-Wave Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave Server to the latest version to fix this issue.", "title": "Newer version of Z-Wave Server needed" + }, + "migrate_unique_id": { + "fix_flow": { + "step": { + "confirm": { + "description": "A Z-Wave controller of model {controller_model} with a different ID ({new_unique_id}) than the previously connected controller ({old_unique_id}) was connected to the {config_entry_title} configuration entry.\n\nReasons for a different controller ID could be:\n\n1. The controller was factory reset, with a 3rd party application.\n2. A controller Non Volatile Memory (NVM) backup was restored to the controller, with a 3rd party application.\n3. A different controller was connected to this configuration entry.\n\nIf a different controller was connected, you should instead set up a new configuration entry for the new controller.\n\nIf you are sure that the current controller is the correct controller you can confirm this by pressing Submit, and the configuration entry will remember the new controller ID.", + "title": "An unknown controller was detected" + } + } + }, + "title": "An unknown controller was detected" } }, "services": { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e4e757ad363..609a0229bcf 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -843,7 +843,11 @@ async def integration_fixture( platforms: list[Platform], ) -> MockConfigEntry: """Set up the zwave_js integration.""" - entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org"}, + unique_id=str(client.driver.controller.home_id), + ) entry.add_to_hass(hass) with patch("homeassistant.components.zwave_js.PLATFORMS", platforms): await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index 1d0f74c7269..d8c3de92b3b 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -12,6 +12,7 @@ from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, issue_registry as ir +from tests.common import MockConfigEntry from tests.components.repairs import ( async_process_repairs_platforms, process_repair_fix_flow, @@ -268,3 +269,118 @@ async def test_abort_confirm( assert data["type"] == "abort" assert data["reason"] == "cannot_connect" assert data["description_placeholders"] == {"device_name": device.name} + + +@pytest.mark.usefixtures("client") +async def test_migrate_unique_id( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the migrate unique id flow.""" + old_unique_id = "123456789" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://test.org", + }, + unique_id=old_unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + issue_id = issue["issue_id"] + assert issue_id == f"migrate_unique_id.{config_entry.entry_id}" + + data = await start_repair_fix_flow(http_client, DOMAIN, issue_id) + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + assert data["description_placeholders"] == { + "config_entry_title": "Z-Wave JS", + "controller_model": "ZW090", + "new_unique_id": "3245146787", + "old_unique_id": old_unique_id, + } + + # Apply fix + data = await process_repair_fix_flow(http_client, flow_id) + + assert data["type"] == "create_entry" + assert config_entry.unique_id == "3245146787" + + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 + + +@pytest.mark.usefixtures("client") +async def test_migrate_unique_id_missing_config_entry( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the migrate unique id flow with missing config entry.""" + old_unique_id = "123456789" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://test.org", + }, + unique_id=old_unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + issue_id = issue["issue_id"] + assert issue_id == f"migrate_unique_id.{config_entry.entry_id}" + + await hass.config_entries.async_remove(config_entry.entry_id) + + assert not hass.config_entries.async_get_entry(config_entry.entry_id) + + data = await start_repair_fix_flow(http_client, DOMAIN, issue_id) + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + assert data["description_placeholders"] == { + "config_entry_title": "Z-Wave JS", + "controller_model": "ZW090", + "new_unique_id": "3245146787", + "old_unique_id": old_unique_id, + } + + # Apply fix + data = await process_repair_fix_flow(http_client, flow_id) + + assert data["type"] == "create_entry" + + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 From 9de1d3b1434df9881d21bf6039be530d17977fff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 12 May 2025 19:37:45 +0200 Subject: [PATCH 041/109] Fill in Plaato URL via placeholders (#144754) --- homeassistant/components/plaato/config_flow.py | 7 ++++++- homeassistant/components/plaato/strings.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 9adfb4a14fe..6a05b209f2c 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -32,6 +32,8 @@ from .const import ( PLACEHOLDER_WEBHOOK_URL, ) +AUTH_TOKEN_URL = "https://intercom.help/plaato/en/articles/5004720-auth_token" + class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" @@ -153,7 +155,10 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN): step_id="api_method", data_schema=data_schema, errors=errors, - description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name}, + description_placeholders={ + PLACEHOLDER_DEVICE_TYPE: device_type.name, + "auth_token_url": AUTH_TOKEN_URL, + }, ) async def _get_webhook_id(self): diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index 23568258118..3fb593a9c73 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -11,7 +11,7 @@ }, "api_method": { "title": "Select API method", - "description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank", + "description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions]({auth_token_url})\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank", "data": { "use_webhook": "Use webhook", "token": "Paste Auth Token here" From 139b48440f8506aa56ce17328a4bc43d24c9d10c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 16 May 2025 12:58:28 +0200 Subject: [PATCH 042/109] Cleanup wrongly combined Reolink devices (#144771) --- homeassistant/components/reolink/__init__.py | 121 ++++++++++++------- homeassistant/components/reolink/util.py | 9 +- tests/components/reolink/test_init.py | 62 +++++++++- 3 files changed, 147 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 433af396d63..48b5dc1a3d6 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -364,53 +364,90 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) + for dev_id in device.identifiers: + (device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host) + if not device_uid: + continue - 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 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) - 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: + 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 ) - else: - _LOGGER.warning( - "Reolink device with uid %s already exists, " - "removing device with uid %s", - new_device_id, - device_uid, + + 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_remove_device(device.id) + 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 + ): + 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, + ) + 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/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index 17e666ac52c..a80e9f8962c 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -76,13 +76,18 @@ def get_store(hass: HomeAssistant, config_entry_id: str) -> Store[str]: def get_device_uid_and_ch( - device: dr.DeviceEntry, host: ReolinkHost + device: dr.DeviceEntry | tuple[str, str], host: ReolinkHost ) -> tuple[list[str], int | None, bool]: """Get the channel and the split device_uid from a reolink DeviceEntry.""" device_uid = [] is_chime = False - for dev_id in device.identifiers: + if isinstance(device, dr.DeviceEntry): + dev_ids = device.identifiers + else: + dev_ids = {device} + + for dev_id in dev_ids: if dev_id[0] == DOMAIN: device_uid = dev_id[1].split("_") if device_uid[0] == host.unique_id: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 6b57c1c253f..f2ae22913ad 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -630,7 +630,7 @@ async def test_cleanup_mac_connection( domain = Platform.SWITCH dev_entry = device_registry.async_get_or_create( - identifiers={(DOMAIN, dev_id)}, + identifiers={(DOMAIN, dev_id), ("OTHER_INTEGRATION", "SOME_ID")}, connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, config_entry_id=config_entry.entry_id, disabled_by=None, @@ -664,6 +664,66 @@ async def test_cleanup_mac_connection( reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM +async def test_cleanup_combined_with_NVR( + 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 combined with the NVR device.""" + reolink_connect.channels = [0] + reolink_connect.baichuan.mac_address.return_value = None + 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), + (DOMAIN, TEST_UID), + ("OTHER_INTEGRATION", "SOME_ID"), + } + + dev_entry = device_registry.async_get_or_create( + identifiers=start_identifiers, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, + 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 == {(DOMAIN, dev_id)} + host_device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_UID)}) + assert host_device + assert host_device.identifiers == { + (DOMAIN, TEST_UID), + ("OTHER_INTEGRATION", "SOME_ID"), + } + + reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From c373fa9296922ba568d6242025d9f05a3733dd91 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 16 May 2025 10:26:00 +0200 Subject: [PATCH 043/109] Do not show an empty component name on MQTT device subentries not as `None` if it is not set (#144792) --- homeassistant/components/mqtt/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 74f55afabaa..4445462003f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -2063,7 +2063,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): entities = [ SelectOptionDict( value=key, - label=f"{device_name} {component_data.get(CONF_NAME, '-')}" + label=f"{device_name} {component_data.get(CONF_NAME, '-') or '-'}" f" ({component_data[CONF_PLATFORM]})", ) for key, component_data in self._subentry_data["components"].items() @@ -2295,7 +2295,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): self._component_id = None mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME] mqtt_items = ", ".join( - f"{mqtt_device} {component_data.get(CONF_NAME, '-')} ({component_data[CONF_PLATFORM]})" + f"{mqtt_device} {component_data.get(CONF_NAME, '-') or '-'} " + f"({component_data[CONF_PLATFORM]})" for component_data in self._subentry_data["components"].values() ) menu_options = [ From d82feb807f320f8b4de89a60ea255e420c66bf2b Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 13 May 2025 10:46:46 +0200 Subject: [PATCH 044/109] Fix blocking call in azure storage (#144803) --- .../components/azure_storage/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/azure_storage/__init__.py b/homeassistant/components/azure_storage/__init__.py index f22e7b70c12..00e419fd3c9 100644 --- a/homeassistant/components/azure_storage/__init__.py +++ b/homeassistant/components/azure_storage/__init__.py @@ -39,11 +39,20 @@ async def async_setup_entry( session = async_create_clientsession( hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60) ) - container_client = ContainerClient( - account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/", - container_name=entry.data[CONF_CONTAINER_NAME], - credential=entry.data[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=session), + + def create_container_client() -> ContainerClient: + """Create a ContainerClient.""" + + return ContainerClient( + account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/", + container_name=entry.data[CONF_CONTAINER_NAME], + credential=entry.data[CONF_STORAGE_ACCOUNT_KEY], + transport=AioHttpTransport(session=session), + ) + + # has a blocking call to open in cpython + container_client: ContainerClient = await hass.async_add_executor_job( + create_container_client ) try: From 6c3a4f17f08af42c471e221c5ff9e02804132391 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 13 May 2025 13:12:00 +0200 Subject: [PATCH 045/109] Fix Z-Wave unique id after controller reset (#144813) --- homeassistant/components/zwave_js/api.py | 23 +++++++++++ .../components/zwave_js/config_flow.py | 26 +----------- homeassistant/components/zwave_js/helpers.py | 26 ++++++++++++ tests/components/zwave_js/conftest.py | 40 ++++++++++++++++++ tests/components/zwave_js/test_api.py | 40 ++++++++++++++++++ tests/components/zwave_js/test_config_flow.py | 41 +------------------ 6 files changed, 133 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index f4397737234..ddfd0cb003d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -71,6 +71,7 @@ from homeassistant.components.websocket_api import ( ActiveConnection, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -88,13 +89,16 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + LOGGER, RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( + CannotConnect, async_enable_statistics, async_get_node_from_device_id, async_get_provisioning_entry_from_device_id, + async_get_version_info, get_device_id, ) @@ -2857,6 +2861,25 @@ async def websocket_hard_reset_controller( async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + # When resetting the controller, the controller home id is also changed. + # The controller state in the client is stale after resetting the controller, + # 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 reset" + ) + else: + hass.config_entries.async_update_entry( + entry, unique_id=str(version_info.home_id) + ) 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 407af9e902b..e52a5e784e8 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -9,14 +9,13 @@ import logging from pathlib import Path from typing import Any -import aiohttp from awesomeversion import AwesomeVersion from serial.tools import list_ports import voluptuous as vol from zwave_js_server.client import Client from zwave_js_server.exceptions import FailedCommand from zwave_js_server.model.driver import Driver -from zwave_js_server.version import VersionInfo, get_server_version +from zwave_js_server.version import VersionInfo from homeassistant.components import usb from homeassistant.components.hassio import ( @@ -36,7 +35,6 @@ from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo @@ -69,6 +67,7 @@ from .const import ( DOMAIN, RESTORE_NVM_DRIVER_READY_TIMEOUT, ) +from .helpers import CannotConnect, async_get_version_info _LOGGER = logging.getLogger(__name__) @@ -79,7 +78,6 @@ ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" -SERVER_VERSION_TIMEOUT = 10 ADDON_LOG_LEVELS = { "error": "Error", @@ -130,22 +128,6 @@ async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: raise InvalidInput("cannot_connect") from err -async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: - """Return Z-Wave JS version info.""" - try: - async with asyncio.timeout(SERVER_VERSION_TIMEOUT): - version_info: VersionInfo = await get_server_version( - ws_address, async_get_clientsession(hass) - ) - except (TimeoutError, aiohttp.ClientError) as err: - # We don't want to spam the log if the add-on isn't started - # or takes a long time to start. - _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) - raise CannotConnect from err - - return version_info - - def get_usb_ports() -> dict[str, str]: """Return a dict of USB ports and their friendly names.""" ports = list_ports.comports() @@ -1357,10 +1339,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return client.driver -class CannotConnect(HomeAssistantError): - """Indicate connection error.""" - - class InvalidInput(HomeAssistantError): """Error to indicate input data is invalid.""" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index ded87b590a4..bfa093f7db9 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -2,11 +2,13 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from dataclasses import astuple, dataclass import logging from typing import Any, cast +import aiohttp import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -25,6 +27,7 @@ from zwave_js_server.model.value import ( ValueDataType, get_value_id_str, ) +from zwave_js_server.version import VersionInfo, get_server_version from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -38,6 +41,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.group import expand_entity_ids from homeassistant.helpers.typing import ConfigType, VolSchemaType @@ -54,6 +58,8 @@ from .const import ( LOGGER, ) +SERVER_VERSION_TIMEOUT = 10 + @dataclass class ZwaveValueID: @@ -568,3 +574,23 @@ def get_network_identifier_for_notification( return f"`{config_entry.title}`, with the home ID `{home_id}`," return f"with the home ID `{home_id}`" return "" + + +async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: + """Return Z-Wave JS version info.""" + try: + async with asyncio.timeout(SERVER_VERSION_TIMEOUT): + version_info: VersionInfo = await get_server_version( + ws_address, async_get_clientsession(hass) + ) + except (TimeoutError, aiohttp.ClientError) as err: + # We don't want to spam the log if the add-on isn't started + # or takes a long time to start. + LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) + raise CannotConnect from err + + return version_info + + +class CannotConnect(HomeAssistantError): + """Indicate connection error.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 609a0229bcf..e0485ced091 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,6 +1,7 @@ """Provide common Z-Wave JS fixtures.""" import asyncio +from collections.abc import Generator import copy import io from typing import Any, cast @@ -15,6 +16,7 @@ from zwave_js_server.version import VersionInfo from homeassistant.components.zwave_js import PLATFORMS from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.util.json import JsonArrayType @@ -587,6 +589,44 @@ def mock_client_fixture( yield client +@pytest.fixture(name="server_version_side_effect") +def server_version_side_effect_fixture() -> Any | None: + """Return the server version side effect.""" + return None + + +@pytest.fixture(name="get_server_version", autouse=True) +def mock_get_server_version( + server_version_side_effect: Any | None, server_version_timeout: int +) -> Generator[AsyncMock]: + """Mock server version.""" + version_info = VersionInfo( + driver_version="mock-driver-version", + server_version="mock-server-version", + home_id=1234, + min_schema_version=0, + max_schema_version=1, + ) + with ( + patch( + "homeassistant.components.zwave_js.helpers.get_server_version", + side_effect=server_version_side_effect, + return_value=version_info, + ) as mock_version, + patch( + "homeassistant.components.zwave_js.helpers.SERVER_VERSION_TIMEOUT", + new=server_version_timeout, + ), + ): + yield mock_version + + +@pytest.fixture(name="server_version_timeout") +def mock_server_version_timeout() -> int: + """Patch the timeout for getting server version.""" + return SERVER_VERSION_TIMEOUT + + @pytest.fixture(name="multisensor_6") def multisensor_6_fixture(client, multisensor_6_state) -> Node: """Mock a multisensor 6 node.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index c6ce3d9ac1b..a3f08513b70 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -7,6 +7,7 @@ import json from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch +from aiohttp import ClientError import pytest from zwave_js_server.const import ( ExclusionStrategy, @@ -5080,14 +5081,17 @@ async def test_subscribe_node_statistics( async def test_hard_reset_controller( hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, client: MagicMock, + get_server_version: AsyncMock, integration: MockConfigEntry, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the hard_reset_controller WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) + assert entry.unique_id == "3245146787" async def async_send_command_driver_ready( message: dict[str, Any], @@ -5122,6 +5126,40 @@ async def test_hard_reset_controller( assert client.async_send_command.call_args_list[0] == call( {"command": "driver.hard_reset"}, 25 ) + 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!") + + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device is not None + assert msg["result"] == device.id + assert msg["success"] + + assert client.async_send_command.call_count == 3 + # The first call is the relevant hard reset command. + # 25 is the require_schema parameter. + assert client.async_send_command.call_args_list[0] == call( + {"command": "driver.hard_reset"}, 25 + ) + assert ( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller reset" + ) in caplog.text client.async_send_command.reset_mock() @@ -5162,6 +5200,8 @@ async def test_hard_reset_controller( {"command": "driver.hard_reset"}, 25 ) + client.async_send_command.reset_mock() + # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.driver.Driver.async_hard_reset", diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 15fd9fcbd30..ac420564f3f 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -17,8 +17,9 @@ from zwave_js_server.exceptions import FailedCommand from zwave_js_server.version import VersionInfo from homeassistant import config_entries, data_entry_flow -from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE +from homeassistant.components.zwave_js.config_flow import TITLE from homeassistant.components.zwave_js.const import ADDON_SLUG, CONF_USB_PATH, DOMAIN +from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo @@ -89,44 +90,6 @@ def mock_supervisor_fixture() -> Generator[None]: yield -@pytest.fixture(name="server_version_side_effect") -def server_version_side_effect_fixture() -> Any | None: - """Return the server version side effect.""" - return None - - -@pytest.fixture(name="get_server_version", autouse=True) -def mock_get_server_version( - server_version_side_effect: Any | None, server_version_timeout: int -) -> Generator[AsyncMock]: - """Mock server version.""" - version_info = VersionInfo( - driver_version="mock-driver-version", - server_version="mock-server-version", - home_id=1234, - min_schema_version=0, - max_schema_version=1, - ) - with ( - patch( - "homeassistant.components.zwave_js.config_flow.get_server_version", - side_effect=server_version_side_effect, - return_value=version_info, - ) as mock_version, - patch( - "homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT", - new=server_version_timeout, - ), - ): - yield mock_version - - -@pytest.fixture(name="server_version_timeout") -def mock_server_version_timeout() -> int: - """Patch the timeout for getting server version.""" - return SERVER_VERSION_TIMEOUT - - @pytest.fixture(name="addon_setup_time", autouse=True) def mock_addon_setup_time() -> Generator[None]: """Mock add-on setup sleep time.""" From b7c07209b8ecda7c19286a9e8ea1fcc75e15e1dd Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 13 May 2025 14:23:41 +0200 Subject: [PATCH 046/109] Fix blocking call in azure_storage config flow (#144818) * Fix blocking call in azure_storage config flow * Fix blocking call in azure_storage config_flow as well * move session getting to event flow --- .../components/azure_storage/config_flow.py | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/azure_storage/config_flow.py b/homeassistant/components/azure_storage/config_flow.py index 2862d290f95..25bd39a6608 100644 --- a/homeassistant/components/azure_storage/config_flow.py +++ b/homeassistant/components/azure_storage/config_flow.py @@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__) class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for azure storage.""" - def get_account_url(self, account_name: str) -> str: - """Get the account URL.""" - return f"https://{account_name}.blob.core.windows.net/" + async def get_container_client( + self, account_name: str, container_name: str, storage_account_key: str + ) -> ContainerClient: + """Get the container client. + + ContainerClient has a blocking call to open in cpython + """ + + session = async_get_clientsession(self.hass) + + def create_container_client() -> ContainerClient: + return ContainerClient( + account_url=f"https://{account_name}.blob.core.windows.net/", + container_name=container_name, + credential=storage_account_key, + transport=AioHttpTransport(session=session), + ) + + return await self.hass.async_add_executor_job(create_container_client) async def validate_config( self, container_client: ContainerClient @@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match( {CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]} ) - container_client = ContainerClient( - account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]), + container_client = await self.get_container_client( + account_name=user_input[CONF_ACCOUNT_NAME], container_name=user_input[CONF_CONTAINER_NAME], - credential=user_input[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=async_get_clientsession(self.hass)), + storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY], ) errors = await self.validate_config(container_client) @@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): reauth_entry = self._get_reauth_entry() if user_input is not None: - container_client = ContainerClient( - account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]), + container_client = await self.get_container_client( + account_name=reauth_entry.data[CONF_ACCOUNT_NAME], container_name=reauth_entry.data[CONF_CONTAINER_NAME], - credential=user_input[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=async_get_clientsession(self.hass)), + storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY], ) + errors = await self.validate_config(container_client) if not errors: return self.async_update_reload_and_abort( @@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): reconfigure_entry = self._get_reconfigure_entry() if user_input is not None: - container_client = ContainerClient( - account_url=self.get_account_url( - reconfigure_entry.data[CONF_ACCOUNT_NAME] - ), + container_client = await self.get_container_client( + account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME], container_name=user_input[CONF_CONTAINER_NAME], - credential=user_input[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=async_get_clientsession(self.hass)), + storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY], ) errors = await self.validate_config(container_client) if not errors: From d9cbd1b65f868a6f2184bef7c87ae0b4365224a4 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 14 May 2025 06:04:38 +0200 Subject: [PATCH 047/109] Bump pylamarzocco to 2.0.3 (#144825) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 1fbef073394..d948d46ef1f 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.2"] + "requirements": ["pylamarzocco==2.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67230b4bfcb..4bb97042255 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.2 +pylamarzocco==2.0.3 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3de21584bfe..a54cb3f3057 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.2 +pylamarzocco==2.0.3 # homeassistant.components.lastfm pylast==5.1.0 From 8161ce6ea85d391f74d3794f15fb3d7d39bda403 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 14 May 2025 05:42:43 -0400 Subject: [PATCH 048/109] Bump python-snoo to 0.6.6 (#144849) --- homeassistant/components/snoo/manifest.json | 2 +- homeassistant/components/snoo/strings.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 839382b2d84..2afec990e4b 100644 --- a/homeassistant/components/snoo/manifest.json +++ b/homeassistant/components/snoo/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["snoo"], "quality_scale": "bronze", - "requirements": ["python-snoo==0.6.5"] + "requirements": ["python-snoo==0.6.6"] } diff --git a/homeassistant/components/snoo/strings.json b/homeassistant/components/snoo/strings.json index 1c86c066c7f..e4a5c634a68 100644 --- a/homeassistant/components/snoo/strings.json +++ b/homeassistant/components/snoo/strings.json @@ -56,7 +56,8 @@ "power": "Power button pressed", "status_requested": "Status requested", "sticky_white_noise_updated": "Sleepytime sounds updated", - "config_change": "Config changed" + "config_change": "Config changed", + "restart": "Restart" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 4bb97042255..b07208625f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ python-roborock==2.18.2 python-smarttub==0.0.39 # homeassistant.components.snoo -python-snoo==0.6.5 +python-snoo==0.6.6 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a54cb3f3057..5613b58face 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2023,7 +2023,7 @@ python-roborock==2.18.2 python-smarttub==0.0.39 # homeassistant.components.snoo -python-snoo==0.6.5 +python-snoo==0.6.6 # homeassistant.components.songpal python-songpal==0.16.2 From 3123a7b1685fa6f71f95d15d872b7714f0454c91 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 14 May 2025 02:42:22 -0700 Subject: [PATCH 049/109] Bump ical to 9.2.4 (#144852) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index b43ded01d6e..d6f2ee76615 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"] + "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 2fa603d51ff..07de4a82244 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.2"] + "requirements": ["ical==9.2.4"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 735c11e645a..367c75d5755 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==9.2.2"] + "requirements": ["ical==9.2.4"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 33a46ea3dc8..9cf39b7ce45 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.2"] + "requirements": ["ical==9.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index b07208625f4..3d348388b93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1200,7 +1200,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.2 +ical==9.2.4 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5613b58face..4fc92103165 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.2 +ical==9.2.4 # homeassistant.components.caldav icalendar==6.1.0 From 5a83627dc5002b199dc1e5b5fdfb25a3a496e316 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 14 May 2025 16:08:24 +1000 Subject: [PATCH 050/109] Fix wall connector states in Teslemetry (#144855) * Fix wall connector * Update snapshot --- homeassistant/components/teslemetry/entity.py | 3 ++- homeassistant/components/teslemetry/sensor.py | 3 +-- tests/components/teslemetry/snapshots/test_sensor.ambr | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 9ce812980db..4bc63fea5e2 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -9,6 +9,7 @@ from tesla_fleet_api.teslemetry import EnergySite, Vehicle from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -229,7 +230,7 @@ class TeslemetryWallConnectorEntity(TeslemetryEntity): super().__init__(data.live_coordinator, key) @property - def _value(self) -> int: + def _value(self) -> StateType: """Return a specific wall connector value from coordinator data.""" return ( self.coordinator.data.get("wall_connectors", {}) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index b87bd334e8c..3567069011d 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -1763,8 +1763,7 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" - if self.exists: - self._attr_native_value = self.entity_description.value_fn(self._value) + self._attr_native_value = self.entity_description.value_fn(self._value) class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity): diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 8e9ce51e297..3b860039b03 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -4978,7 +4978,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle-statealt] @@ -4991,7 +4991,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle_2-entry] @@ -5038,7 +5038,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle_2-statealt] @@ -5051,7 +5051,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors_streaming[sensor.test_battery_level-state] From 4c4be883231b357a6373b1fcb85575bdefd28d67 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 14 May 2025 11:58:29 +0200 Subject: [PATCH 051/109] Fix Reolink setup when ONVIF push is unsupported (#144869) * Fix setup when ONVIF push is not supported * fix styling --- homeassistant/components/reolink/host.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 378c167d469..c3a8d340501 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -581,7 +581,12 @@ class ReolinkHost: ) return - await self._api.subscribe(self._webhook_url) + try: + await self._api.subscribe(self._webhook_url) + except NotSupportedError as err: + self._onvif_push_supported = False + _LOGGER.debug(err) + return _LOGGER.debug( "Host %s: subscribed successfully to webhook %s", @@ -602,7 +607,11 @@ class ReolinkHost: return # API is shutdown, no need to subscribe try: - if self._onvif_push_supported and not self._api.baichuan.events_active: + if ( + self._onvif_push_supported + and not self._api.baichuan.events_active + and self._cancel_tcp_push_check is None + ): await self._renew(SubType.push) if self._onvif_long_poll_supported and self._long_poll_task is not None: From e5e1c9fb0581c09fd74c29ba104ff6e573d0b2ca Mon Sep 17 00:00:00 2001 From: rjblake Date: Fri, 16 May 2025 10:37:11 +0200 Subject: [PATCH 052/109] Fix some Home Connect translation strings (#144905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update strings.json Corrected program names: changed "Pre_rinse" to "Pre-Rinse" changed "Kurz 60°C" to "Speed 60°C" Both match the Home Connect app; although the UK documentation refers to "Speed 60°C" as "Quick 60°C" * Adjust casing --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/home_connect/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 19d7cc06046..7e364a6aa50 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -234,7 +234,7 @@ "consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye", "consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye", "consumer_products_coffee_maker_program_beverage_hot_water": "Hot water", - "dishcare_dishwasher_program_pre_rinse": "Pre_rinse", + "dishcare_dishwasher_program_pre_rinse": "Pre-rinse", "dishcare_dishwasher_program_auto_1": "Auto 1", "dishcare_dishwasher_program_auto_2": "Auto 2", "dishcare_dishwasher_program_auto_3": "Auto 3", @@ -252,7 +252,7 @@ "dishcare_dishwasher_program_intensiv_power": "Intensive power", "dishcare_dishwasher_program_magic_daily": "Magic daily", "dishcare_dishwasher_program_super_60": "Super 60ºC", - "dishcare_dishwasher_program_kurz_60": "Kurz 60ºC", + "dishcare_dishwasher_program_kurz_60": "Speed 60ºC", "dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC", "dishcare_dishwasher_program_machine_care": "Machine care", "dishcare_dishwasher_program_steam_fresh": "Steam fresh", From bf1d2069e4b6409fe2d503e4ae8e8954b55e6835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 14 May 2025 19:25:01 +0200 Subject: [PATCH 053/109] Update Tibber lib 0.31.2 (#144908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 3a3a772a934..43cbd79afef 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tibber", "iot_class": "cloud_polling", "loggers": ["tibber"], - "requirements": ["pyTibber==0.30.8"] + "requirements": ["pyTibber==0.31.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3d348388b93..dd2f06568a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1804,7 +1804,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.30.8 +pyTibber==0.31.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4fc92103165..ec35f07b3c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1491,7 +1491,7 @@ pyHomee==1.2.8 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.30.8 +pyTibber==0.31.2 # homeassistant.components.dlink pyW215==0.7.0 From 543104b36c797639e11c80469b521f19e2f59698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 14 May 2025 20:46:28 +0200 Subject: [PATCH 054/109] Update mill library 0.12.5 (#144911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update mill library 0.12.5 Signed-off-by: Daniel Hjelseth Høyer * Update mill library 0.12.5 Signed-off-by: Daniel Hjelseth Høyer --------- Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/coordinator.py | 4 ++-- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mill/coordinator.py b/homeassistant/components/mill/coordinator.py index 288b341b0f9..a701acb8ddb 100644 --- a/homeassistant/components/mill/coordinator.py +++ b/homeassistant/components/mill/coordinator.py @@ -26,7 +26,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -TWO_YEARS = 2 * 365 * 24 +TWO_YEARS_DAYS = 2 * 365 class MillDataUpdateCoordinator(DataUpdateCoordinator): @@ -91,7 +91,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): if not last_stats or not last_stats.get(statistic_id): hourly_data = ( await self.mill_data_connection.fetch_historic_energy_usage( - dev_id, n_days=TWO_YEARS + dev_id, n_days=TWO_YEARS_DAYS ) ) hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0])) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index bfad9b48cb9..c5cc94ead30 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.12.3", "mill-local==0.3.0"] + "requirements": ["millheater==0.12.5", "mill-local==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd2f06568a5..2557955164d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1427,7 +1427,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.3 +millheater==0.12.5 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec35f07b3c7..6952c71ca64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1200,7 +1200,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.3 +millheater==0.12.5 # homeassistant.components.minio minio==7.1.12 From a657964c25c3248f9cf9e91ae1a17d65b46950fb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 15 May 2025 09:27:48 +0200 Subject: [PATCH 055/109] Fix unknown Pure AQI in Sensibo (#144924) * Fix unknown Pure AQI in Sensibo * Fix mypy --- homeassistant/components/sensibo/climate.py | 2 +- homeassistant/components/sensibo/manifest.json | 2 +- homeassistant/components/sensibo/sensor.py | 16 ++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/test_sensor.py | 13 ++++++++++++- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 906c4259ce5..a40cb110f66 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -252,7 +252,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): return features @property - def current_humidity(self) -> int | None: + def current_humidity(self) -> float | None: """Return the current humidity.""" return self.device_data.humidity diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 610695aaf7b..4cadd3f8692 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -15,5 +15,5 @@ "iot_class": "cloud_polling", "loggers": ["pysensibo"], "quality_scale": "platinum", - "requirements": ["pysensibo==1.1.0"] + "requirements": ["pysensibo==1.2.1"] } diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 09f095bfaec..bab85eb2294 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -101,14 +101,25 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( value_fn=lambda data: data.temperature, ), ) + + +def _pure_aqi(pm25_pure: PureAQI | None) -> str | None: + """Return the Pure aqi name or None if unknown.""" + if pm25_pure: + aqi_name = pm25_pure.name.lower() + if aqi_name != "unknown": + return aqi_name + return None + + PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="pm25", translation_key="pm25_pure", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: data.pm25_pure.name.lower() if data.pm25_pure else None, + value_fn=lambda data: _pure_aqi(data.pm25_pure), extra_fn=None, - options=[aqi.name.lower() for aqi in PureAQI], + options=[aqi.name.lower() for aqi in PureAQI if aqi.name != "UNKNOWN"], ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", @@ -119,6 +130,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( FILTER_LAST_RESET_DESCRIPTION, ) + DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="timer_time", diff --git a/requirements_all.txt b/requirements_all.txt index 2557955164d..98d38adcdca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2293,7 +2293,7 @@ pysaj==0.0.16 pyschlage==2025.4.0 # homeassistant.components.sensibo -pysensibo==1.1.0 +pysensibo==1.2.1 # homeassistant.components.serial pyserial-asyncio-fast==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6952c71ca64..4b8ff028a79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1875,7 +1875,7 @@ pysabnzbd==1.1.1 pyschlage==2025.4.0 # homeassistant.components.sensibo -pysensibo==1.1.0 +pysensibo==1.2.1 # homeassistant.components.acer_projector # homeassistant.components.crownstone diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index 8ea76036123..7b7450b97a4 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -11,7 +11,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -45,3 +45,14 @@ async def test_sensor( state = hass.states.get("sensor.kitchen_pure_aqi") assert state.state == "moderate" + + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pm25_pure = PureAQI(0) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.kitchen_pure_aqi") + assert state.state == STATE_UNKNOWN From f086f4a9556050002c4257ad97c531f0b95bb94a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 16 May 2025 12:58:59 +0200 Subject: [PATCH 056/109] Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue for new setups (#144940) --- homeassistant/components/fronius/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index b8aa2da81c6..97e040abf98 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -35,7 +35,7 @@ async def validate_host( hass: HomeAssistant, host: str ) -> tuple[str, FroniusConfigEntryData]: """Validate the user input allows us to connect.""" - fronius = Fronius(async_get_clientsession(hass), host) + fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host) try: datalogger_info: dict[str, Any] From a9520888cf0a2b6e8c090cf76dad818664557e25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 May 2025 16:07:53 +0200 Subject: [PATCH 057/109] Fix Home Assistant Yellow config entry data (#144948) --- .../homeassistant_yellow/__init__.py | 9 +-- .../homeassistant_yellow/config_flow.py | 7 +- .../homeassistant_yellow/test_config_flow.py | 4 +- .../homeassistant_yellow/test_init.py | 71 +++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 71aa8ef99b7..27c40e35946 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -90,16 +90,17 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version=2, ) - if config_entry.minor_version == 2: - # Add a `firmware_version` key + if config_entry.minor_version <= 3: + # Add a `firmware_version` key if it doesn't exist to handle entries created + # with minor version 1.3 where the firmware version was not set. hass.config_entries.async_update_entry( config_entry, data={ **config_entry.data, - FIRMWARE_VERSION: None, + FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION), }, version=1, - minor_version=3, + minor_version=4, ) _LOGGER.debug( diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 5472c346e94..1fac6bcac96 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN): """Handle a config flow for Home Assistant Yellow.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 def __init__(self, *args: Any, **kwargs: Any) -> None: """Instantiate config flow.""" @@ -116,6 +116,11 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN): if self._probed_firmware_info is not None else ApplicationType.EZSP ).value, + FIRMWARE_VERSION: ( + self._probed_firmware_info.firmware_version + if self._probed_firmware_info is not None + else None + ), }, ) diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 46fec0a1f30..1d5a64eafb9 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -101,12 +101,12 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" - assert result["data"] == {"firmware": "ezsp"} + assert result["data"] == {"firmware": "ezsp", "firmware_version": None} assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == {"firmware": "ezsp"} + assert config_entry.data == {"firmware": "ezsp", "firmware_version": None} assert config_entry.options == {} assert config_entry.title == "Home Assistant Yellow" diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 57d63c7441e..00e3383cf77 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -10,6 +10,9 @@ from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, FirmwareInfo, ) +from homeassistant.components.homeassistant_yellow.config_flow import ( + HomeAssistantYellowConfigFlow, +) from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -248,3 +251,71 @@ async def test_setup_entry_addon_info_fails( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + ("start_version", "data", "migrated_data"), + [ + (1, {}, {"firmware": "ezsp", "firmware_version": None}), + (2, {"firmware": "ezsp"}, {"firmware": "ezsp", "firmware_version": None}), + ( + 2, + {"firmware": "ezsp", "firmware_version": "123"}, + {"firmware": "ezsp", "firmware_version": "123"}, + ), + (3, {"firmware": "ezsp"}, {"firmware": "ezsp", "firmware_version": None}), + ( + 3, + {"firmware": "ezsp", "firmware_version": "123"}, + {"firmware": "ezsp", "firmware_version": "123"}, + ), + ], +) +async def test_migrate_entry( + hass: HomeAssistant, + start_version: int, + data: dict, + migrated_data: dict, +) -> None: + """Test migration of a config entry.""" + mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) + + # Setup the config entry + config_entry = MockConfigEntry( + data=data, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + version=1, + minor_version=start_version, + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant_yellow.guess_firmware_info", + return_value=FirmwareInfo( # Nothing is setup + device="/dev/ttyAMA1", + firmware_version="1234", + firmware_type=ApplicationType.EZSP, + source="unknown", + owners=[], + ), + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.data == migrated_data + assert config_entry.options == {} + assert config_entry.minor_version == HomeAssistantYellowConfigFlow.MINOR_VERSION + assert config_entry.version == HomeAssistantYellowConfigFlow.VERSION From 19b7cfbd4a19dbbc408b946996a9cc1e57e47700 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 15 May 2025 13:46:15 +0200 Subject: [PATCH 058/109] Bump deebot-client to 13.2.0 (#144957) --- 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 e670a36cf72..b1674e123fa 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.1.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 98d38adcdca..7ac4c47d7f3 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.1.0 +deebot-client==13.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b8ff028a79..e2fc32ea09e 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.1.0 +deebot-client==13.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 0ba55c31e88dac1540b840815e90918e68268b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 16 May 2025 01:57:16 +0200 Subject: [PATCH 059/109] Fix ESPHome entities unavailable if deep sleep enabled after entry setup (#144970) --- homeassistant/components/esphome/entity.py | 6 ++- tests/components/esphome/test_entity.py | 62 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 7b02680afee..94c4a8ffe46 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -223,7 +223,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): self._states = cast(dict[int, _StateT], entry_data.state[state_type]) assert entry_data.device_info is not None device_info = entry_data.device_info - self._device_info = device_info self._on_entry_data_changed() self._key = entity_info.key self._state_type = state_type @@ -311,6 +310,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): @callback def _on_entry_data_changed(self) -> None: entry_data = self._entry_data + # Update the device info since it can change + # when the device is reconnected + if TYPE_CHECKING: + assert entry_data.device_info is not None + self._device_info = entry_data.device_info self._api_version = entry_data.api_version self._client = entry_data.client if self._device_info.has_deep_sleep: diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index ee6e6b6785f..36185efeb72 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -1,6 +1,7 @@ """Test ESPHome binary sensors.""" import asyncio +from dataclasses import asdict from typing import Any from unittest.mock import AsyncMock @@ -8,6 +9,7 @@ from aioesphomeapi import ( APIClient, BinarySensorInfo, BinarySensorState, + DeviceInfo, SensorInfo, SensorState, build_unique_id, @@ -665,3 +667,63 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage( ) state = hass.states.get("binary_sensor.user_named") assert state is not None + + +async def test_deep_sleep_added_after_setup( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test deep sleep added after setup.""" + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[ + BinarySensorInfo( + object_id="test", + key=1, + name="test", + unique_id="test", + ), + ], + user_service=[], + states=[ + BinarySensorState(key=1, state=True, missing_state=False), + ], + device_info={"has_deep_sleep": False}, + ) + + entity_id = "binary_sensor.test_test" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(expected_disconnect=True) + + # No deep sleep, should be unavailable + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await mock_device.mock_connect() + + # reconnect, should be available + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(expected_disconnect=True) + new_device_info = DeviceInfo( + **{**asdict(mock_device.device_info), "has_deep_sleep": True} + ) + mock_device.client.device_info = AsyncMock(return_value=new_device_info) + mock_device.device_info = new_device_info + + await mock_device.mock_connect() + + # Now disconnect that deep sleep is set in device info + await mock_device.mock_disconnect(expected_disconnect=True) + + # Deep sleep, should be available + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON From 9f0db9874569c051b51b2050f8a900acef4bde9d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 16 May 2025 04:25:24 -0400 Subject: [PATCH 060/109] Strip `_CLIENT` suffix from ZHA event `unique_id` (#145006) --- homeassistant/components/zha/helpers.py | 15 ++++- tests/components/zha/test_device_action.py | 64 ++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index c819f94ceba..084e1c882ac 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -419,13 +419,26 @@ class ZHADeviceProxy(EventBase): @callback def handle_zha_event(self, zha_event: ZHAEvent) -> None: """Handle a ZHA event.""" + if ATTR_UNIQUE_ID in zha_event.data: + unique_id = zha_event.data[ATTR_UNIQUE_ID] + + # Client cluster handler unique IDs in the ZHA lib were disambiguated by + # adding a suffix of `_CLIENT`. Unfortunately, this breaks existing + # automations that match the `unique_id` key. This can be removed in a + # future release with proper notice of a breaking change. + unique_id = unique_id.removesuffix("_CLIENT") + else: + unique_id = zha_event.unique_id + self.gateway_proxy.hass.bus.async_fire( ZHA_EVENT, { ATTR_DEVICE_IEEE: str(zha_event.device_ieee), - ATTR_UNIQUE_ID: zha_event.unique_id, ATTR_DEVICE_ID: self.device_id, **zha_event.data, + # The order of these keys is intentional, `zha_event.data` can contain + # a `unique_id` key, which we explicitly replace + ATTR_UNIQUE_ID: unique_id, }, ) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 6708250e448..becf9d81557 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -258,3 +258,67 @@ async def test_invalid_zha_event_type( # `zha_send_event` accepts only zigpy responses, lists, and dicts with pytest.raises(TypeError): cluster_handler.zha_send_event(COMMAND_SINGLE, 123) + + +async def test_client_unique_id_suffix_stripped( + hass: HomeAssistant, setup_zha, zigpy_device_mock +) -> None: + """Test that the `_CLIENT_` unique ID suffix is stripped.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "zha_event", + "event_data": { + "unique_id": "38:5b:44:ff:fe:a7:cc:69:1:0x0006", # no `_CLIENT` suffix + "endpoint_id": 1, + "cluster_id": 6, + "command": "on", + "args": [], + "params": {}, + }, + }, + "action": {"service": "zha.test"}, + } + }, + ) + + service_calls = async_mock_service(hass, DOMAIN, "test") + + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + ) + + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + + zha_device.emit_zha_event( + { + "unique_id": "38:5b:44:ff:fe:a7:cc:69:1:0x0006_CLIENT", + "endpoint_id": 1, + "cluster_id": 6, + "command": "on", + "args": [], + "params": {}, + } + ) + + await hass.async_block_till_done(wait_background_tasks=True) + assert len(service_calls) == 1 From 715f116954c4863f23ec7e19bc1bbbd0990cad91 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 13:10:58 +0200 Subject: [PATCH 061/109] Bump pySmartThings to 3.2.2 (#145033) --- 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 043bdea71e2..f72405dae20 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.1"] + "requirements": ["pysmartthings==3.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7ac4c47d7f3..3697f8450c3 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.1 +pysmartthings==3.2.2 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2fc32ea09e..8d472a0f0e5 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.1 +pysmartthings==3.2.2 # homeassistant.components.smarty pysmarty2==0.10.2 From 0691ad93627206b7802b73134819784fd0e9291f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 13:17:56 +0200 Subject: [PATCH 062/109] Set SmartThings oven setpoint to unknown if its 1 Fahrenheit (#145038) --- homeassistant/components/smartthings/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 2d6451fa279..219e1dfe5c1 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -584,7 +584,7 @@ CAPABILITY_TO_SENSORS: dict[ device_class=SensorDeviceClass.TEMPERATURE, use_temperature_unit=True, # Set the value to None if it is 0 F (-17 C) - value_fn=lambda value: None if value in {0, -17} else value, + value_fn=lambda value: None if value in {-17, 0, 1} else value, ) ] }, From b76ac68fb11f5c6555aceaf8ed3638cfda36163a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 16 May 2025 19:51:30 +0300 Subject: [PATCH 063/109] Fix climate idle state for Comelit (#145059) --- homeassistant/components/comelit/climate.py | 4 +--- tests/components/comelit/snapshots/test_climate.ambr | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index be5b892e53c..e7890cddff8 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -134,11 +134,9 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): self._attr_current_temperature = values[0] / 10 self._attr_hvac_action = None - if _mode == ClimaComelitMode.OFF: - self._attr_hvac_action = HVACAction.OFF if not _active: self._attr_hvac_action = HVACAction.IDLE - if _mode in API_STATUS: + elif _mode in API_STATUS: self._attr_hvac_action = API_STATUS[_mode]["hvac_action"] self._attr_hvac_mode = None diff --git a/tests/components/comelit/snapshots/test_climate.ambr b/tests/components/comelit/snapshots/test_climate.ambr index e5201067ee1..0233359bc45 100644 --- a/tests/components/comelit/snapshots/test_climate.ambr +++ b/tests/components/comelit/snapshots/test_climate.ambr @@ -48,7 +48,7 @@ 'attributes': ReadOnlyDict({ 'current_temperature': 22.1, 'friendly_name': 'Climate0', - 'hvac_action': , + 'hvac_action': , 'hvac_modes': list([ , , From e2ede3ed19770225643ec10ba8ff74c3172aa4aa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 20:14:41 +0200 Subject: [PATCH 064/109] Map SmartThings auto mode correctly (#145061) --- .../components/smartthings/climate.py | 8 ++++---- .../smartthings/snapshots/test_climate.ambr | 20 +++++++++---------- tests/components/smartthings/test_climate.py | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index f2f9479584c..983609b895f 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.HEAT_COOL, + "auto": HVACMode.AUTO, "cool": HVACMode.COOL, "eco": HVACMode.AUTO, "rush hour": HVACMode.AUTO, @@ -40,7 +40,7 @@ MODE_TO_STATE = { "off": HVACMode.OFF, } STATE_TO_MODE = { - HVACMode.HEAT_COOL: "auto", + HVACMode.AUTO: "auto", HVACMode.COOL: "cool", HVACMode.HEAT: "heat", HVACMode.OFF: "off", @@ -58,7 +58,7 @@ OPERATING_STATE_TO_ACTION = { } AC_MODE_TO_STATE = { - "auto": HVACMode.HEAT_COOL, + "auto": HVACMode.AUTO, "cool": HVACMode.COOL, "dry": HVACMode.DRY, "coolClean": HVACMode.COOL, @@ -69,7 +69,7 @@ AC_MODE_TO_STATE = { "wind": HVACMode.FAN_ONLY, } STATE_TO_AC_MODE = { - HVACMode.HEAT_COOL: "auto", + HVACMode.AUTO: "auto", HVACMode.COOL: "cool", HVACMode.DRY: "dry", HVACMode.HEAT: "heat", diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index 633b02568fc..b23e7024e05 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -146,7 +146,7 @@ , , , - , + , , ]), 'max_temp': 35, @@ -206,7 +206,7 @@ , , , - , + , , ]), 'max_temp': 35, @@ -246,7 +246,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -308,7 +308,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -349,7 +349,7 @@ ]), 'hvac_modes': list([ , - , + , , , , @@ -414,7 +414,7 @@ 'friendly_name': 'Aire Dormitorio Principal', 'hvac_modes': list([ , - , + , , , , @@ -462,7 +462,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -513,7 +513,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -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 138601ec08b..48cbf4fb4ed 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -119,7 +119,7 @@ async def test_ac_set_hvac_mode_off( @pytest.mark.parametrize( ("hvac_mode", "argument"), [ - (HVACMode.HEAT_COOL, "auto"), + (HVACMode.AUTO, "auto"), (HVACMode.COOL, "cool"), (HVACMode.DRY, "dry"), (HVACMode.HEAT, "heat"), @@ -174,7 +174,7 @@ async def test_ac_set_hvac_mode_turns_on( SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: "climate.ac_office_granit", - ATTR_HVAC_MODE: HVACMode.HEAT_COOL, + ATTR_HVAC_MODE: HVACMode.AUTO, }, blocking=True, ) @@ -266,7 +266,7 @@ async def test_ac_set_temperature_and_hvac_mode_while_off( { ATTR_ENTITY_ID: "climate.ac_office_granit", ATTR_TEMPERATURE: 23, - ATTR_HVAC_MODE: HVACMode.HEAT_COOL, + ATTR_HVAC_MODE: HVACMode.AUTO, }, blocking=True, ) @@ -316,7 +316,7 @@ async def test_ac_set_temperature_and_hvac_mode( { ATTR_ENTITY_ID: "climate.ac_office_granit", ATTR_TEMPERATURE: 23, - ATTR_HVAC_MODE: HVACMode.HEAT_COOL, + ATTR_HVAC_MODE: HVACMode.AUTO, }, blocking=True, ) @@ -623,7 +623,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.HEAT_COOL}, + {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.AUTO}, blocking=True, ) devices.execute_device_command.assert_called_once_with( From 146e440d5979990cd2670766d38c41192eaeb5b3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 16 May 2025 19:37:22 +0200 Subject: [PATCH 065/109] Update frontend to 20250516.0 (#145062) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9471f863a72..5c5feca98b7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250509.0"] + "requirements": ["home-assistant-frontend==20250516.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a42910fc7e2..abed9e8cab2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250509.0 +home-assistant-frontend==20250516.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3697f8450c3..79ae3501792 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.72 # homeassistant.components.frontend -home-assistant-frontend==20250509.0 +home-assistant-frontend==20250516.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d472a0f0e5..f3686c8e39b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.72 # homeassistant.components.frontend -home-assistant-frontend==20250509.0 +home-assistant-frontend==20250516.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From 4906e78a5cf33a3f96656e49c8cc54c23e78d289 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 19:53:46 +0200 Subject: [PATCH 066/109] Only set suggested area for new SmartThings devices (#145063) --- .../components/smartthings/__init__.py | 17 ++++++++-- tests/components/smartthings/test_init.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index cec71f91750..ff03ce5ca67 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -32,6 +32,7 @@ from homeassistant.const import ( ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_SUGGESTED_AREA, ATTR_SW_VERSION, ATTR_VIA_DEVICE, CONF_ACCESS_TOKEN, @@ -453,14 +454,24 @@ def create_devices( ATTR_SW_VERSION: viper.software_version, } ) + if ( + device_registry.async_get_device({(DOMAIN, device.device.device_id)}) + is None + ): + kwargs.update( + { + ATTR_SUGGESTED_AREA: ( + rooms.get(device.device.room_id) + if device.device.room_id + else None + ) + } + ) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, device.device.device_id)}, configuration_url="https://account.smartthings.com", name=device.device.label, - suggested_area=( - rooms.get(device.device.room_id) if device.device.room_id else None - ), **kwargs, ) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 1d4b124c60d..fcb962449bf 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -59,6 +59,37 @@ async def test_devices( assert device == snapshot +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) +async def test_device_not_resetting_area( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device not resetting area.""" + await setup_integration(hass, mock_config_entry) + + device_id = devices.get_devices.return_value[0].device_id + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + + assert device.area_id == "theater" + + device_registry.async_update_device(device_id=device.id, area_id=None) + await hass.async_block_till_done() + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + + assert device.area_id is None + + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + assert device.area_id is None + + @pytest.mark.parametrize("device_fixture", ["button"]) async def test_button_event( hass: HomeAssistant, From 621a14d7ccb640d89bd2b0e2aadae6f1c0a6cb3e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 19:38:18 +0200 Subject: [PATCH 067/109] Fix fan AC mode in SmartThings AC (#145064) --- homeassistant/components/smartthings/climate.py | 17 ++++++++++------- .../fixtures/device_status/da_ac_rac_01001.json | 2 +- tests/components/smartthings/test_climate.py | 8 +++++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 983609b895f..7cb3b0210bb 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -66,6 +66,7 @@ AC_MODE_TO_STATE = { "heat": HVACMode.HEAT, "heatClean": HVACMode.HEAT, "fanOnly": HVACMode.FAN_ONLY, + "fan": HVACMode.FAN_ONLY, "wind": HVACMode.FAN_ONLY, } STATE_TO_AC_MODE = { @@ -88,6 +89,7 @@ FAN_OSCILLATION_TO_SWING = { } WIND = "wind" +FAN = "fan" WINDFREE = "windFree" UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT} @@ -388,14 +390,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): tasks.append(self.async_turn_on()) mode = STATE_TO_AC_MODE[hvac_mode] - # If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" mode the AirConditioner new mode has to be "wind" - # The conversion make the mode change working - # The conversion is made only for device that wrongly has capability "wind" instead "fan_only" + # If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" or "fan" mode the AirConditioner + # new mode has to be "wind" or "fan" if hvac_mode == HVACMode.FAN_ONLY: - if WIND in self.get_attribute_value( - Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES - ): - mode = WIND + for fan_mode in (WIND, FAN): + if fan_mode in self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES + ): + mode = fan_mode + break tasks.append( self.execute_device_command( diff --git a/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json b/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json index e8e71c53ace..3982e1174f4 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json +++ b/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json @@ -32,7 +32,7 @@ "timestamp": "2025-02-09T14:35:56.800Z" }, "supportedAcModes": { - "value": ["auto", "cool", "dry", "wind", "heat", "dryClean"], + "value": ["auto", "cool", "dry", "fan", "heat", "dryClean"], "timestamp": "2025-02-09T15:42:13.444Z" }, "airConditionerMode": { diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 48cbf4fb4ed..8241e6de3b3 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -196,17 +196,19 @@ async def test_ac_set_hvac_mode_turns_on( @pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) -async def test_ac_set_hvac_mode_wind( +@pytest.mark.parametrize("mode", ["fan", "wind"]) +async def test_ac_set_hvac_mode_fan( hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry, + mode: str, ) -> None: """Test setting AC HVAC mode to wind if the device supports it.""" set_attribute_value( devices, Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES, - ["auto", "cool", "dry", "heat", "wind"], + ["auto", "cool", "dry", "heat", mode], ) set_attribute_value(devices, Capability.SWITCH, Attribute.SWITCH, "on") @@ -223,7 +225,7 @@ async def test_ac_set_hvac_mode_wind( Capability.AIR_CONDITIONER_MODE, Command.SET_AIR_CONDITIONER_MODE, MAIN, - argument="wind", + argument=mode, ) From 8c4eec231f37624dc43aec65ad399d62df61d350 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 21:17:07 +0200 Subject: [PATCH 068/109] Don't create entities for Smartthings smarttags (#145066) --- .../components/smartthings/__init__.py | 11 ++ tests/components/smartthings/conftest.py | 1 + .../device_status/im_smarttag2_ble_uwb.json | 129 ++++++++++++ .../devices/im_smarttag2_ble_uwb.json | 184 ++++++++++++++++++ .../smartthings/snapshots/test_init.ambr | 33 ++++ 5 files changed, 358 insertions(+) create mode 100644 tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json create mode 100644 tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index ff03ce5ca67..557d14f8a64 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -13,6 +13,7 @@ from aiohttp import ClientResponseError from pysmartthings import ( Attribute, Capability, + Category, ComponentStatus, Device, DeviceEvent, @@ -194,6 +195,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry) } devices = await client.get_devices() for device in devices: + if ( + (main_component := device.components.get(MAIN)) is not None + and main_component.manufacturer_category is Category.BLUETOOTH_TRACKER + ): + device_status[device.device_id] = FullDevice( + device=device, + status={}, + online=True, + ) + continue status = process_status(await client.get_device_status(device.device_id)) online = await client.get_device_health(device.device_id) device_status[device.device_id] = FullDevice( diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b3a58b17637..253a01b6d5f 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -150,6 +150,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "generic_ef00_v1", "bosch_radiator_thermostat_ii", "im_speaker_ai_0001", + "im_smarttag2_ble_uwb", "abl_light_b_001", "tplink_p110", "ikea_kadrilj", diff --git a/tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json b/tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json new file mode 100644 index 00000000000..e59db7476de --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json @@ -0,0 +1,129 @@ +{ + "components": { + "main": { + "tag.e2eEncryption": { + "encryption": { + "value": null + } + }, + "audioVolume": { + "volume": { + "value": null + } + }, + "geofence": { + "enableState": { + "value": null + }, + "geofence": { + "value": null + }, + "name": { + "value": null + } + }, + "tag.updatedInfo": { + "connection": { + "value": "connected", + "timestamp": "2024-02-27T17:44:57.638Z" + } + }, + "tag.factoryReset": {}, + "battery": { + "quantity": { + "value": null + }, + "battery": { + "value": null + }, + "type": { + "value": null + } + }, + "firmwareUpdate": { + "lastUpdateStatusReason": { + "value": null + }, + "availableVersion": { + "value": null + }, + "lastUpdateStatus": { + "value": null + }, + "supportedCommands": { + "value": null + }, + "state": { + "value": null + }, + "updateAvailable": { + "value": false, + "timestamp": "2024-06-25T05:56:22.227Z" + }, + "currentVersion": { + "value": null + }, + "lastUpdateTime": { + "value": null + } + }, + "tag.searchingStatus": { + "searchingStatus": { + "value": null + } + }, + "tag.tagStatus": { + "connectedUserId": { + "value": null + }, + "tagStatus": { + "value": null + }, + "connectedDeviceId": { + "value": null + } + }, + "alarm": { + "alarm": { + "value": null + } + }, + "tag.tagButton": { + "tagButton": { + "value": null + } + }, + "tag.uwbActivation": { + "uwbActivation": { + "value": null + } + }, + "geolocation": { + "method": { + "value": null + }, + "heading": { + "value": null + }, + "latitude": { + "value": null + }, + "accuracy": { + "value": null + }, + "altitudeAccuracy": { + "value": null + }, + "speed": { + "value": null + }, + "longitude": { + "value": null + }, + "lastUpdateTime": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json b/tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json new file mode 100644 index 00000000000..802b4da1514 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json @@ -0,0 +1,184 @@ +{ + "items": [ + { + "deviceId": "83d660e4-b0c8-4881-a674-d9f1730366c1", + "name": "Tag(UWB)", + "label": "SmartTag+ black", + "manufacturerName": "Samsung Electronics", + "presentationId": "IM-SmartTag-BLE-UWB", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "redacted_locid", + "ownerId": "redacted", + "roomId": "redacted_roomid", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "alarm", + "version": 1 + }, + { + "id": "tag.tagButton", + "version": 1 + }, + { + "id": "audioVolume", + "version": 1 + }, + { + "id": "battery", + "version": 1 + }, + { + "id": "tag.factoryReset", + "version": 1 + }, + { + "id": "tag.e2eEncryption", + "version": 1 + }, + { + "id": "tag.tagStatus", + "version": 1 + }, + { + "id": "geolocation", + "version": 1 + }, + { + "id": "geofence", + "version": 1 + }, + { + "id": "tag.uwbActivation", + "version": 1 + }, + { + "id": "tag.updatedInfo", + "version": 1 + }, + { + "id": "tag.searchingStatus", + "version": 1 + }, + { + "id": "firmwareUpdate", + "version": 1 + } + ], + "categories": [ + { + "name": "BluetoothTracker", + "categoryType": "manufacturer" + } + ] + } + ], + "createTime": "2023-05-25T09:42:59.720Z", + "profile": { + "id": "e443f3e8-a926-3deb-917c-e5c6de3af70f" + }, + "bleD2D": { + "encryptionKey": "ZTbd_04NISrhQODE7_i8JdcG2ZWwqmUfY60taptK7J0=", + "cipher": "AES_128-CBC-PKCS7Padding", + "identifier": "415D4Y16F97F", + "configurationVersion": "2.0", + "configurationUrl": "https://apis.samsungiotcloud.com/v1/miniature/profile/b8e65e7e-6152-4704-b9f5-f16352034237", + "bleDeviceType": "BLE", + "metadata": { + "regionCode": 11, + "privacyIdPoolSize": 2000, + "privacyIdSeed": "AAAAAAAX8IQ=", + "privacyIdInitialVector": "ZfqZKLRGSeCwgNhdqHFRpw==", + "numAllowableConnections": 2, + "firmware": { + "version": "1.03.07", + "specVersion": "0.5.6", + "updateTime": 1685007914000, + "latestFirmware": { + "id": 581, + "version": "1.03.07", + "data": { + "checksum": "50E7", + "size": "586004", + "supportedVersion": "0.5.6" + } + } + }, + "currentServerTime": 1739095473, + "searchingStatus": "stop", + "lastKnownConnection": { + "updated": 1713422813, + "connectedUser": { + "id": "sk3oyvsbkm", + "name": "" + }, + "connectedDevice": { + "id": "4f3faa4c-976c-3bd8-b209-607f3a5a9814", + "name": "" + }, + "d2dStatus": "bleScanned", + "nearby": true, + "onDemand": false + }, + "e2eEncryption": { + "enabled": false + }, + "timer": 1713422675, + "category": { + "id": 0 + }, + "remoteRing": { + "enabled": false + }, + "petWalking": { + "enabled": false + }, + "onboardedBy": { + "saGuid": "sk3oyvsbkm" + }, + "shareable": { + "enabled": false + }, + "agingCounter": { + "status": "VALID", + "updated": 1713422675 + }, + "vendor": { + "mnId": "0AFD", + "setupId": "432", + "modelName": "EI-T7300" + }, + "priorityConnection": { + "lba": false, + "cameraShutter": false + }, + "createTime": 1685007780, + "updateTime": 1713422675, + "fmmSearch": false, + "ooTime": { + "currentOoTime": 8, + "defaultOoTime": 8 + }, + "pidPoolSize": { + "desiredPidPoolSize": 2000, + "currentPidPoolSize": 2000 + }, + "activeMode": { + "mode": 0 + }, + "itemConfig": { + "searchingStatus": "stop" + } + } + }, + "type": "BLE_D2D", + "restrictionTier": 0, + "allowed": [], + "executionContext": "CLOUD" + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index d70d9a1dcfc..596cc487dd5 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1487,6 +1487,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[im_smarttag2_ble_uwb] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '83d660e4-b0c8-4881-a674-d9f1730366c1', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'SmartTag+ black', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[im_speaker_ai_0001] DeviceRegistryEntrySnapshot({ 'area_id': None, From 34455f97433408a0d66ab3364478c9d4aa77bb69 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 16 May 2025 21:22:43 +0200 Subject: [PATCH 069/109] Fix Ecovacs mower area sensors (#145071) --- homeassistant/components/ecovacs/sensor.py | 39 +++++++++++++++++-- .../ecovacs/snapshots/test_sensor.ambr | 8 ++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 6c8ae080fc3..a8600d786a8 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -6,7 +6,8 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic -from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan +from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType +from deebot_client.device import Device from deebot_client.events import ( BatteryEvent, ErrorEvent, @@ -34,7 +35,7 @@ from homeassistant.const import ( UnitOfArea, UnitOfTime, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType @@ -59,6 +60,15 @@ class EcovacsSensorEntityDescription( """Ecovacs sensor entity description.""" value_fn: Callable[[EventT], StateType] + native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None + + +@callback +def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None: + """Get the area native unit of measurement based on device type.""" + if device_type is DeviceType.MOWER: + return UnitOfArea.SQUARE_CENTIMETERS + return UnitOfArea.SQUARE_METERS ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( @@ -68,7 +78,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.area, translation_key="stats_area", - native_unit_of_measurement=UnitOfArea.SQUARE_METERS, + native_unit_of_measurement_fn=get_area_native_unit_of_measurement, ), EcovacsSensorEntityDescription[StatsEvent]( key="stats_time", @@ -85,7 +95,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( value_fn=lambda e: e.area, key="total_stats_area", translation_key="total_stats_area", - native_unit_of_measurement=UnitOfArea.SQUARE_METERS, + native_unit_of_measurement_fn=get_area_native_unit_of_measurement, state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[TotalStatsEvent]( @@ -249,6 +259,27 @@ class EcovacsSensor( entity_description: EcovacsSensorEntityDescription + def __init__( + self, + device: Device, + capability: CapabilityEvent, + entity_description: EcovacsSensorEntityDescription, + **kwargs: Any, + ) -> None: + """Initialize entity.""" + super().__init__(device, capability, entity_description, **kwargs) + if ( + entity_description.native_unit_of_measurement_fn + and ( + native_unit_of_measurement + := entity_description.native_unit_of_measurement_fn( + device.capabilities.device_type + ) + ) + is not None + ): + self._attr_native_unit_of_measurement = native_unit_of_measurement + async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" await super().async_added_to_hass() diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index c4e5a5b1966..7fa7a41234d 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -181,14 +181,14 @@ '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({ 'friendly_name': 'Goat G1 Area cleaned', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_area_cleaned', @@ -523,7 +523,7 @@ '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] @@ -531,7 +531,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Goat G1 Total area cleaned', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_total_area_cleaned', From 02b028add36c51d0c6b1f4dc9dc062c796cf048e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 May 2025 19:31:36 +0000 Subject: [PATCH 070/109] Bump version to 2025.5.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 65f8e2bae64..9e3149fd89a 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 = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index ad3f9ad86e2..cc11f10d3cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.1" +version = "2025.5.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 0ef098a9f39c359cc279b9f944ff9f068acb0e39 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 May 2025 22:39:05 +0200 Subject: [PATCH 071/109] Pin rpds-py to 0.24.0 (#145074) --- homeassistant/package_constraints.txt | 5 +++++ script/gen_requirements_all.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index abed9e8cab2..11b1233bcda 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -217,3 +217,8 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 + +# rpds-py > 0.25.0 requires cargo 1.84.0 +# Stable Alpine current only ships cargo 1.83.0 +# No wheels upstream available for armhf & armv7 +rpds-py==0.24.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b4e18ea5962..307a9c42d53 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -246,6 +246,11 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 + +# rpds-py > 0.25.0 requires cargo 1.84.0 +# Stable Alpine current only ships cargo 1.83.0 +# No wheels upstream available for armhf & armv7 +rpds-py==0.24.0 """ GENERATED_MESSAGE = ( From c33372686754467093b4686a851a40d3f1a739ee Mon Sep 17 00:00:00 2001 From: wuede Date: Sun, 18 May 2025 23:00:36 +0200 Subject: [PATCH 072/109] 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 073/109] 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 074/109] 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 075/109] 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 076/109] 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 077/109] 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 078/109] 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 079/109] 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 080/109] 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 081/109] 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 082/109] 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 083/109] 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 084/109] 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 085/109] 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 086/109] 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 087/109] 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 088/109] 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 089/109] 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 090/109] 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 091/109] 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 092/109] 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 093/109] 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 094/109] 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 095/109] 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 096/109] 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 097/109] 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 098/109] 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 099/109] 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 100/109] 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 101/109] 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 102/109] 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 103/109] 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 104/109] 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 105/109] 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 106/109] 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 107/109] 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 108/109] 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 109/109] 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."