From 83534f286ed338571791284485dd0e47dc3127a3 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 7 Jun 2026 12:23:09 +0200 Subject: [PATCH 1/4] Ensure opentherm_gw boiler and thermostat manufacturers are strings (#173162) --- .../components/opentherm_gw/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 9b560db52e356e..c8a1ba449e73f6 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -159,11 +159,14 @@ async def handle_report(status): _LOGGER.debug("Received report: %s", status) async_dispatcher_send(self.hass, self.update_signal, status) + boiler_manufacturer = status[OpenThermDataSource.BOILER].get( + gw_vars.DATA_SLAVE_MEMBERID + ) dev_reg.async_update_device( boiler_device.id, - manufacturer=status[OpenThermDataSource.BOILER].get( - gw_vars.DATA_SLAVE_MEMBERID - ), + manufacturer=str(boiler_manufacturer) + if boiler_manufacturer is not None + else None, model_id=status[OpenThermDataSource.BOILER].get( gw_vars.DATA_SLAVE_PRODUCT_TYPE ), @@ -175,11 +178,14 @@ async def handle_report(status): ), ) + thermostat_manufacturer = status[OpenThermDataSource.THERMOSTAT].get( + gw_vars.DATA_MASTER_MEMBERID + ) dev_reg.async_update_device( thermostat_device.id, - manufacturer=status[OpenThermDataSource.THERMOSTAT].get( - gw_vars.DATA_MASTER_MEMBERID - ), + manufacturer=str(thermostat_manufacturer) + if thermostat_manufacturer is not None + else None, model_id=status[OpenThermDataSource.THERMOSTAT].get( gw_vars.DATA_MASTER_PRODUCT_TYPE ), From a29f2907f78f00144c26e6d316a0902be7eb7f20 Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Sun, 7 Jun 2026 14:07:48 +0200 Subject: [PATCH 2/4] Use NodeType enum in Duco entity (#173189) --- homeassistant/components/duco/entity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duco/entity.py b/homeassistant/components/duco/entity.py index 233bca42f70121..2d46a754f4209d 100644 --- a/homeassistant/components/duco/entity.py +++ b/homeassistant/components/duco/entity.py @@ -1,6 +1,6 @@ """Base entity for the Duco integration.""" -from duco_connectivity.models import Node +from duco_connectivity.models import Node, NodeType from homeassistant.const import ATTR_VIA_DEVICE from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo @@ -25,7 +25,7 @@ def __init__(self, coordinator: DucoCoordinator, node: Node) -> None: identifiers={(DOMAIN, f"{mac}_{node.node_id}")}, manufacturer="Duco", model=coordinator.board_info.box_name - if node.general.node_type == "BOX" + if node.general.node_type == NodeType.BOX else node.general.node_type, name=node.general.name or f"Node {node.node_id}", ) @@ -34,7 +34,7 @@ def __init__(self, coordinator: DucoCoordinator, node: Node) -> None: "connections": {(CONNECTION_NETWORK_MAC, mac)}, "serial_number": coordinator.board_info.serial_board_box, } - if node.general.node_type == "BOX" + if node.general.node_type == NodeType.BOX else {ATTR_VIA_DEVICE: (DOMAIN, f"{mac}_1")} ) self._attr_device_info = device_info From 3fbdbb12e2a851099f2e9d6d6ff901832dc5c6ce Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 7 Jun 2026 05:12:20 -0700 Subject: [PATCH 3/4] Support streaming updates for V1 Roborock devices (#173182) --- .../components/roborock/coordinator.py | 45 ++++++++++++++++--- tests/components/roborock/test_init.py | 32 +++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 622831afdfceb2..05df95178306dc 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -1,5 +1,6 @@ """Roborock Coordinator.""" +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -21,7 +22,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CONNECTIONS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -117,6 +118,7 @@ def __init__( # to the base class. This is reset on successful data update. self._last_update_success_time: datetime | None = None self._has_connected_locally: bool = False + self._unsubs: list[Callable[[], None]] = [] @cached_property def dock_device_info(self) -> DeviceInfo: @@ -169,6 +171,15 @@ async def _async_setup(self) -> None: # Force a map refresh on first setup self.last_home_update = dt_util.utcnow() - IMAGE_CACHE_INTERVAL + self._unsubs.append( + self.properties_api.status.add_update_listener(self._handle_trait_update) + ) + self._unsubs.append( + self.properties_api.consumables.add_update_listener( + self._handle_trait_update + ) + ) + async def update_map(self) -> None: """Update the currently selected map.""" try: @@ -266,12 +277,7 @@ async def _async_update_data(self) -> DeviceState | None: self.last_update_state = self.properties_api.status.state_name self._last_update_success_time = dt_util.utcnow() _LOGGER.debug("Data update successful %s", self._last_update_success_time) - return DeviceState( - status=self.properties_api.status, - dnd_timer=self.properties_api.dnd, - consumable=self.properties_api.consumables, - clean_summary=self.properties_api.clean_summary, - ) + return self._device_state def _should_suppress_update_failure(self) -> bool: """Determine if we should suppress update failure reporting. @@ -290,6 +296,31 @@ def _should_suppress_update_failure(self) -> bool: _LOGGER.debug("Update failure duration: %s", failure_duration) return failure_duration < MIN_UNAVAILABLE_DURATION + @property + def _device_state(self) -> DeviceState: + """Return the current device state.""" + return DeviceState( + status=self.properties_api.status, + dnd_timer=self.properties_api.dnd, + consumable=self.properties_api.consumables, + clean_summary=self.properties_api.clean_summary, + ) + + @callback + def _handle_trait_update(self) -> None: + """Handle trait updates from push notifications.""" + _LOGGER.debug("Trait updated, updating coordinator data") + self.async_set_updated_data(self._device_state) + # We optimize streaming updates to catch state transitions immediately, but + # secondary updates (like refreshing the map) can happen on their own interval. + + async def async_shutdown(self) -> None: + """Shutdown coordinator and unsubscribe update listeners.""" + await super().async_shutdown() + for unsub in self._unsubs: + unsub() + self._unsubs.clear() + async def get_routines(self) -> list[HomeDataScene]: """Get routines.""" try: diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 822258d9814e7c..59cfaa85a88377 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -748,3 +748,35 @@ async def test_all_devices_disabled( ) assert device_entry is not None assert device_entry.disabled + + +@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) +async def test_v1_streaming_updates( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + fake_vacuum: FakeDevice, +) -> None: + """Test that V1 push updates update entity states immediately.""" + assert setup_entry.state is ConfigEntryState.LOADED + + sensor_entity_id = "sensor.roborock_s7_maxv_battery" + state = hass.states.get(sensor_entity_id) + assert state is not None + assert state.state == "100" + + # Verify that add_update_listener was called on the mock status trait + status_trait = fake_vacuum.v1_properties.status + assert status_trait.add_update_listener.called + + # Get the registered callback + callback_func = status_trait.add_update_listener.call_args[0][0] # type: ignore[union-attr] + + # Update a status attribute and trigger the callback + status_trait.battery = 85 + callback_func() + await hass.async_block_till_done() + + # Check if the state was updated in Home Assistant immediately + state = hass.states.get(sensor_entity_id) + assert state is not None + assert state.state == "85" From 9c9695d0ba2f041f595821fe736faa9920ccf554 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:19:26 +0200 Subject: [PATCH 4/4] Bump uiprotect to 11.3.0 (#173024) Co-authored-by: RaHehl --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 82baade26ac5c6..f1dd4f05f9f4f0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["uiprotect"], "quality_scale": "platinum", - "requirements": ["uiprotect==10.5.0"] + "requirements": ["uiprotect==11.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 95cf7d38cc72fc..27d9fce6c2f14d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3246,7 +3246,7 @@ uasiren==0.0.1 uhooapi==1.2.8 # homeassistant.components.unifiprotect -uiprotect==10.5.0 +uiprotect==11.3.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.6.1